schematichq 1.5.1 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.fern/metadata.json +3 -3
- data/.fern/replay.lock +14 -338
- data/.fernignore +6 -0
- data/README.md +141 -0
- data/WASM_VERSION +1 -1
- data/custom.gemspec.rb +6 -0
- data/lib/schematic/accounts/types/update_environment_request_body.rb +2 -0
- data/lib/schematic/billing/types/create_invoice_request_body.rb +2 -0
- data/lib/schematic/client.rb +1 -1
- data/lib/schematic/credits/client.rb +50 -6
- data/lib/schematic/credits/leases/check.rb +477 -0
- data/lib/schematic/credits/leases/lease_manager.rb +565 -0
- data/lib/schematic/credits/leases/lease_store.rb +240 -0
- data/lib/schematic/credits/leases/redis_lease_store.rb +358 -0
- data/lib/schematic/credits/leases/redis_reservation_store.rb +326 -0
- data/lib/schematic/credits/leases/reservation_store.rb +161 -0
- data/lib/schematic/credits/leases/server_check.rb +237 -0
- data/lib/schematic/credits/leases/track.rb +66 -0
- data/lib/schematic/credits/leases/types.rb +329 -0
- data/lib/schematic/credits/leases/wire_client.rb +83 -0
- data/lib/schematic/credits/types/acquire_credit_lease_request_body.rb +2 -0
- data/lib/schematic/credits/types/create_credit_spend_policy_request_body.rb +5 -1
- data/lib/schematic/credits/types/extend_credit_lease_request_body.rb +2 -0
- data/lib/schematic/credits/types/get_credit_spend_policy_usage_params.rb +16 -0
- data/lib/schematic/credits/types/get_credit_spend_policy_usage_request.rb +15 -0
- data/lib/schematic/credits/types/get_credit_spend_policy_usage_response.rb +13 -0
- data/lib/schematic/credits/types/update_credit_spend_policy_request_body.rb +4 -0
- data/lib/schematic/datastream/client.rb +32 -2
- data/lib/schematic/entitlements/client.rb +165 -0
- data/lib/schematic/entitlements/types/count_company_user_usage_params.rb +24 -0
- data/lib/schematic/entitlements/types/count_company_user_usage_request.rb +23 -0
- data/lib/schematic/entitlements/types/count_company_user_usage_response.rb +13 -0
- data/lib/schematic/entitlements/types/get_company_user_usage_metrics_params.rb +16 -0
- data/lib/schematic/entitlements/types/get_company_user_usage_metrics_request.rb +15 -0
- data/lib/schematic/entitlements/types/get_company_user_usage_metrics_response.rb +13 -0
- data/lib/schematic/entitlements/types/list_company_user_usage_params.rb +24 -0
- data/lib/schematic/entitlements/types/list_company_user_usage_request.rb +23 -0
- data/lib/schematic/entitlements/types/list_company_user_usage_response.rb +13 -0
- data/lib/schematic/plangroups/client.rb +2 -0
- data/lib/schematic/plangroups/types/create_plan_group_request_body.rb +2 -0
- data/lib/schematic/plangroups/types/update_plan_group_request_body.rb +2 -0
- data/lib/schematic/planmigrations/client.rb +6 -0
- data/lib/schematic/planmigrations/types/count_migrations_params.rb +2 -0
- data/lib/schematic/planmigrations/types/count_migrations_request.rb +2 -0
- data/lib/schematic/planmigrations/types/create_migration_input.rb +2 -0
- data/lib/schematic/planmigrations/types/list_migrations_params.rb +2 -0
- data/lib/schematic/planmigrations/types/list_migrations_request.rb +2 -0
- data/lib/schematic/plans/types/publish_plan_version_request_body.rb +4 -0
- data/lib/schematic/plans/types/retry_custom_plan_billing_request_body.rb +2 -0
- data/lib/schematic/rules_engine.rb +37 -0
- data/lib/schematic/schematic_client.rb +830 -33
- data/lib/schematic/types/capture_raw_event.rb +4 -0
- data/lib/schematic/types/change_subscription_internal_request_body.rb +2 -0
- data/lib/schematic/types/change_subscription_request_body.rb +2 -0
- data/lib/schematic/types/check_flags_response_data.rb +2 -0
- data/lib/schematic/types/company_detail_response_data.rb +2 -0
- data/lib/schematic/types/company_feature_usage_export_metadata_visible_columns_item.rb +1 -0
- data/lib/schematic/types/company_plan_detail_response_data.rb +2 -0
- data/lib/schematic/types/company_user_usage_metrics_response_data.rb +15 -0
- data/lib/schematic/types/company_user_usage_response_data.rb +19 -0
- data/lib/schematic/types/company_user_usage_row_response_data.rb +17 -0
- data/lib/schematic/types/component_display_settings.rb +2 -0
- data/lib/schematic/types/component_settings_response_data.rb +2 -0
- data/lib/schematic/types/credit_event_ledger_response_data.rb +7 -1
- data/lib/schematic/types/credit_event_type.rb +3 -0
- data/lib/schematic/types/credit_ledger_entry_kind.rb +17 -0
- data/lib/schematic/types/credit_spend_policy.rb +25 -0
- data/lib/schematic/types/credit_spend_policy_response_data.rb +12 -0
- data/lib/schematic/types/credit_spend_window.rb +11 -0
- data/lib/schematic/types/credit_spend_window_unit.rb +13 -0
- data/lib/schematic/types/custom_plan_billing_response_data.rb +2 -0
- data/lib/schematic/types/environment_detail_response_data.rb +2 -0
- data/lib/schematic/types/environment_response_data.rb +2 -0
- data/lib/schematic/types/estimated_plan_total.rb +13 -0
- data/lib/schematic/types/invoice_request_body.rb +2 -0
- data/lib/schematic/types/invoice_response_data.rb +2 -0
- data/lib/schematic/types/manage_plan_request.rb +2 -0
- data/lib/schematic/types/pending_migration_response_data.rb +6 -0
- data/lib/schematic/types/plan_version_migration_response_data.rb +2 -0
- data/lib/schematic/types/plan_version_migration_strategy.rb +1 -0
- data/lib/schematic/types/preview_subscription_finance_response_data.rb +2 -0
- data/lib/schematic/types/rules_engine_schema_version.rb +1 -1
- data/lib/schematic/types/rulesengine_company.rb +2 -0
- data/lib/schematic/types/rulesengine_comparable_operator.rb +18 -0
- data/lib/schematic/types/rulesengine_condition.rb +1 -1
- data/lib/schematic/types/rulesengine_credit_spend_policy.rb +25 -0
- data/lib/schematic/types/rulesengine_credit_spend_policy_scope.rb +13 -0
- data/lib/schematic/types/rulesengine_credit_spend_window.rb +11 -0
- data/lib/schematic/types/rulesengine_user.rb +2 -0
- data/lib/schematic/types/upcoming_invoice_response_data.rb +2 -0
- data/lib/schematic/types/user_usage_metric.rb +12 -0
- data/lib/schematic/version.rb +1 -1
- data/lib/schematic/wasm/rulesengine.wasm +0 -0
- data/lib/schematic.rb +27 -2
- data/lib/schematichq.rb +10 -0
- data/reference.md +485 -12
- metadata +37 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 42a7ac411958f446624efef8e76092dec19e652603089848b36d93fbe53a5f3f
|
|
4
|
+
data.tar.gz: 942a73f63c86d6c66f3bf57a369578479e18529943a3b234900e6b87116046e9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 90da04958f952788cb9fa76c46f5c791e2eba2754aec598007b7c66c0a5f80777e325345fe337155055a5301e3b61c482204dde4ef7d74830bb87d0f539a9126
|
|
7
|
+
data.tar.gz: 276cd27a9f3c58009313ff802cf87f09aafc7f689ab5d916e10764dda976dd06a553d7a65e8e69a02b9f358e3409faba975d26eac3d52c222aed8895673cf05f
|
data/.fern/metadata.json
CHANGED
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"webrick": ">= 1.0"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
-
"originGitCommit": "
|
|
16
|
+
"originGitCommit": "6a2f1e8406783d0f2591db2053e1a8a8df4a9046",
|
|
17
17
|
"originGitCommitIsDirty": false,
|
|
18
18
|
"invokedBy": "ci",
|
|
19
|
-
"requestedVersion": "1.5.
|
|
19
|
+
"requestedVersion": "1.5.3",
|
|
20
20
|
"ciProvider": "github",
|
|
21
|
-
"sdkVersion": "1.5.
|
|
21
|
+
"sdkVersion": "1.5.3"
|
|
22
22
|
}
|
data/.fern/replay.lock
CHANGED
|
@@ -108,341 +108,17 @@ generations:
|
|
|
108
108
|
cli_version: unknown
|
|
109
109
|
generator_versions:
|
|
110
110
|
fernapi/fern-ruby-sdk: 1.25.0
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
--- /dev/null
|
|
126
|
-
+++ b/test/rules_engine_clock_test.rb
|
|
127
|
-
@@ -0,0 +1,75 @@
|
|
128
|
-
+# frozen_string_literal: true
|
|
129
|
-
+
|
|
130
|
-
+require_relative "test_helper"
|
|
131
|
-
+require "minitest/autorun"
|
|
132
|
-
+
|
|
133
|
-
+# Regression for SCHY-471.
|
|
134
|
-
+#
|
|
135
|
-
+# A company-override entitlement rule whose metric condition uses a
|
|
136
|
-
+# calendar/billing metric period drives the engine into the metric-period-reset
|
|
137
|
-
+# code path, which needs a wall clock. On the raw wasm32-unknown-unknown build
|
|
138
|
-
+# that path used to trap (+wasm unreachable+); the flag then wrongly fell back to
|
|
139
|
-
+# its default value for a company that is legitimately entitled. The host now
|
|
140
|
-
+# injects the current time via +setCurrentTimeMillis+ so this evaluates cleanly.
|
|
141
|
-
+class RulesEngineClockTest < Minitest::Test
|
|
142
|
-
+ def setup
|
|
143
|
-
+ @engine = Schematic::RulesEngine.new
|
|
144
|
-
+ @engine.initialize!
|
|
145
|
-
+ # The wasm binary is fetched via scripts/download-wasm.sh and may be absent
|
|
146
|
-
+ # (or the wasmtime gem missing) in some environments — skip rather than fail.
|
|
147
|
-
+ skip "WASM rules engine unavailable (wasm binary or wasmtime gem missing)" unless @engine.initialized?
|
|
148
|
-
+ end
|
|
149
|
-
+
|
|
150
|
-
+ def flag
|
|
151
|
-
+ {
|
|
152
|
-
+ "id" => "flag1", "key" => "mcp-access", "account_id" => "acc_1",
|
|
153
|
-
+ "environment_id" => "env_1", "default_value" => false, "rules" => []
|
|
154
|
-
+ }
|
|
155
|
-
+ end
|
|
156
|
-
+
|
|
157
|
-
+ # Usage 40 < allocation 100, so the override grants the feature.
|
|
158
|
-
+ def entitled_company
|
|
159
|
-
+ {
|
|
160
|
-
+ "id" => "co_entitled", "account_id" => "acc_1", "environment_id" => "env_1",
|
|
161
|
-
+ "keys" => { "id" => "co_entitled" },
|
|
162
|
-
+ "metrics" => [{
|
|
163
|
-
+ "account_id" => "acc_1", "environment_id" => "env_1", "company_id" => "co_entitled",
|
|
164
|
-
+ "event_subtype" => "api-calls", "period" => "current_month",
|
|
165
|
-
+ "month_reset" => "billing_cycle", "value" => 40, "created_at" => "2023-01-01T00:00:00Z"
|
|
166
|
-
+ }],
|
|
167
|
-
+ "rules" => [{
|
|
168
|
-
+ "id" => "rule_override", "flag_id" => "flag1", "account_id" => "acc_1",
|
|
169
|
-
+ "environment_id" => "env_1", "name" => "Company Override",
|
|
170
|
-
+ "rule_type" => "company_override", "priority" => 0, "value" => true,
|
|
171
|
-
+ "condition_groups" => [],
|
|
172
|
-
+ "conditions" => [
|
|
173
|
-
+ {
|
|
174
|
-
+ "id" => "cond_company", "account_id" => "acc_1", "environment_id" => "env_1",
|
|
175
|
-
+ "condition_type" => "company", "operator" => "eq",
|
|
176
|
-
+ "resource_ids" => ["co_entitled"], "trait_value" => ""
|
|
177
|
-
+ },
|
|
178
|
-
+ {
|
|
179
|
-
+ "id" => "cond_metric", "account_id" => "acc_1", "environment_id" => "env_1",
|
|
180
|
-
+ "condition_type" => "metric", "operator" => "lt", "event_subtype" => "api-calls",
|
|
181
|
-
+ "metric_value" => 100, "metric_period" => "current_month",
|
|
182
|
-
+ "metric_period_month_reset" => "billing_cycle", "trait_value" => "100"
|
|
183
|
-
+ }
|
|
184
|
-
+ ]
|
|
185
|
-
+ }]
|
|
186
|
-
+ }
|
|
187
|
-
+ end
|
|
188
|
-
+
|
|
189
|
-
+ def test_billing_metric_override_evaluates_without_trapping
|
|
190
|
-
+ result = @engine.check_flag(flag, entitled_company)
|
|
191
|
-
+
|
|
192
|
-
+ assert result[:value], "company override should grant the flag, not fall back to default false"
|
|
193
|
-
+ assert_match(/override/i, result[:reason].to_s)
|
|
194
|
-
+ end
|
|
195
|
-
+
|
|
196
|
-
+ def test_billing_metric_override_populates_reset_at
|
|
197
|
-
+ result = @engine.check_flag(flag, entitled_company)
|
|
198
|
-
+
|
|
199
|
-
+ refute_nil result[:feature_usage_reset_at],
|
|
200
|
-
+ "reset-at should be computed from the injected host time"
|
|
201
|
-
+ end
|
|
202
|
-
+end
|
|
203
|
-
theirs_snapshot:
|
|
204
|
-
test/rules_engine_clock_test.rb: |
|
|
205
|
-
# frozen_string_literal: true
|
|
206
|
-
|
|
207
|
-
require_relative "test_helper"
|
|
208
|
-
require "minitest/autorun"
|
|
209
|
-
|
|
210
|
-
# Regression for SCHY-471.
|
|
211
|
-
#
|
|
212
|
-
# A company-override entitlement rule whose metric condition uses a
|
|
213
|
-
# calendar/billing metric period drives the engine into the metric-period-reset
|
|
214
|
-
# code path, which needs a wall clock. On the raw wasm32-unknown-unknown build
|
|
215
|
-
# that path used to trap (+wasm unreachable+); the flag then wrongly fell back to
|
|
216
|
-
# its default value for a company that is legitimately entitled. The host now
|
|
217
|
-
# injects the current time via +setCurrentTimeMillis+ so this evaluates cleanly.
|
|
218
|
-
class RulesEngineClockTest < Minitest::Test
|
|
219
|
-
def setup
|
|
220
|
-
@engine = Schematic::RulesEngine.new
|
|
221
|
-
@engine.initialize!
|
|
222
|
-
# The wasm binary is fetched via scripts/download-wasm.sh and may be absent
|
|
223
|
-
# (or the wasmtime gem missing) in some environments — skip rather than fail.
|
|
224
|
-
skip "WASM rules engine unavailable (wasm binary or wasmtime gem missing)" unless @engine.initialized?
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def flag
|
|
228
|
-
{
|
|
229
|
-
"id" => "flag1", "key" => "mcp-access", "account_id" => "acc_1",
|
|
230
|
-
"environment_id" => "env_1", "default_value" => false, "rules" => []
|
|
231
|
-
}
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
# Usage 40 < allocation 100, so the override grants the feature.
|
|
235
|
-
def entitled_company
|
|
236
|
-
{
|
|
237
|
-
"id" => "co_entitled", "account_id" => "acc_1", "environment_id" => "env_1",
|
|
238
|
-
"keys" => { "id" => "co_entitled" },
|
|
239
|
-
"metrics" => [{
|
|
240
|
-
"account_id" => "acc_1", "environment_id" => "env_1", "company_id" => "co_entitled",
|
|
241
|
-
"event_subtype" => "api-calls", "period" => "current_month",
|
|
242
|
-
"month_reset" => "billing_cycle", "value" => 40, "created_at" => "2023-01-01T00:00:00Z"
|
|
243
|
-
}],
|
|
244
|
-
"rules" => [{
|
|
245
|
-
"id" => "rule_override", "flag_id" => "flag1", "account_id" => "acc_1",
|
|
246
|
-
"environment_id" => "env_1", "name" => "Company Override",
|
|
247
|
-
"rule_type" => "company_override", "priority" => 0, "value" => true,
|
|
248
|
-
"condition_groups" => [],
|
|
249
|
-
"conditions" => [
|
|
250
|
-
{
|
|
251
|
-
"id" => "cond_company", "account_id" => "acc_1", "environment_id" => "env_1",
|
|
252
|
-
"condition_type" => "company", "operator" => "eq",
|
|
253
|
-
"resource_ids" => ["co_entitled"], "trait_value" => ""
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
"id" => "cond_metric", "account_id" => "acc_1", "environment_id" => "env_1",
|
|
257
|
-
"condition_type" => "metric", "operator" => "lt", "event_subtype" => "api-calls",
|
|
258
|
-
"metric_value" => 100, "metric_period" => "current_month",
|
|
259
|
-
"metric_period_month_reset" => "billing_cycle", "trait_value" => "100"
|
|
260
|
-
}
|
|
261
|
-
]
|
|
262
|
-
}]
|
|
263
|
-
}
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
def test_billing_metric_override_evaluates_without_trapping
|
|
267
|
-
result = @engine.check_flag(flag, entitled_company)
|
|
268
|
-
|
|
269
|
-
assert result[:value], "company override should grant the flag, not fall back to default false"
|
|
270
|
-
assert_match(/override/i, result[:reason].to_s)
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
def test_billing_metric_override_populates_reset_at
|
|
274
|
-
result = @engine.check_flag(flag, entitled_company)
|
|
275
|
-
|
|
276
|
-
refute_nil result[:feature_usage_reset_at],
|
|
277
|
-
"reset-at should be computed from the injected host time"
|
|
278
|
-
end
|
|
279
|
-
end
|
|
280
|
-
user_owned: true
|
|
281
|
-
- id: patch-ee61788c
|
|
282
|
-
content_hash: sha256:e652287219a0a709e16112ced444e6472c6fa8bdfe888edf868caaf9769a9472
|
|
283
|
-
original_commit: ee61788c5a83fce5dd5768b2536391459550d176
|
|
284
|
-
original_message: fix rubocop offenses in clock regression test (SCHY-471)
|
|
285
|
-
original_author: Christopher Brady <chris@schematichq.com>
|
|
286
|
-
base_generation: e22ae14fff5d0a5d3222b98154a1d1bc42b99dd8
|
|
287
|
-
files:
|
|
288
|
-
- test/rules_engine_clock_test.rb
|
|
289
|
-
patch_content: |
|
|
290
|
-
diff --git a/test/rules_engine_clock_test.rb b/test/rules_engine_clock_test.rb
|
|
291
|
-
new file mode 100644
|
|
292
|
-
index 0000000..8711b5e
|
|
293
|
-
--- /dev/null
|
|
294
|
-
+++ b/test/rules_engine_clock_test.rb
|
|
295
|
-
@@ -0,0 +1,75 @@
|
|
296
|
-
+# frozen_string_literal: true
|
|
297
|
-
+
|
|
298
|
-
+require_relative "test_helper"
|
|
299
|
-
+require "minitest/autorun"
|
|
300
|
-
+
|
|
301
|
-
+# Regression for SCHY-471.
|
|
302
|
-
+#
|
|
303
|
-
+# A company-override entitlement rule whose metric condition uses a
|
|
304
|
-
+# calendar/billing metric period drives the engine into the metric-period-reset
|
|
305
|
-
+# code path, which needs a wall clock. On the raw wasm32-unknown-unknown build
|
|
306
|
-
+# that path used to trap (+wasm unreachable+); the flag then wrongly fell back to
|
|
307
|
-
+# its default value for a company that is legitimately entitled. The host now
|
|
308
|
-
+# injects the current time via +setCurrentTimeMillis+ so this evaluates cleanly.
|
|
309
|
-
+class RulesEngineClockTest < Minitest::Test
|
|
310
|
-
+ def setup
|
|
311
|
-
+ @engine = Schematic::RulesEngine.new
|
|
312
|
-
+ @engine.initialize!
|
|
313
|
-
+ # The wasm binary is fetched via scripts/download-wasm.sh and may be absent
|
|
314
|
-
+ # (or the wasmtime gem missing) in some environments — skip rather than fail.
|
|
315
|
-
+ skip "WASM rules engine unavailable (wasm binary or wasmtime gem missing)" unless @engine.initialized?
|
|
316
|
-
+ end
|
|
317
|
-
+
|
|
318
|
-
+ def flag
|
|
319
|
-
+ {
|
|
320
|
-
+ "id" => "flag1", "key" => "mcp-access", "account_id" => "acc_1",
|
|
321
|
-
+ "environment_id" => "env_1", "default_value" => false, "rules" => []
|
|
322
|
-
+ }
|
|
323
|
-
+ end
|
|
324
|
-
+
|
|
325
|
-
+ # Usage 40 < allocation 100, so the override grants the feature.
|
|
326
|
-
+ def entitled_company
|
|
327
|
-
+ {
|
|
328
|
-
+ "id" => "co_entitled", "account_id" => "acc_1", "environment_id" => "env_1",
|
|
329
|
-
+ "keys" => { "id" => "co_entitled" },
|
|
330
|
-
+ "metrics" => [{
|
|
331
|
-
+ "account_id" => "acc_1", "environment_id" => "env_1", "company_id" => "co_entitled",
|
|
332
|
-
+ "event_subtype" => "api-calls", "period" => "current_month",
|
|
333
|
-
+ "month_reset" => "billing_cycle", "value" => 40, "created_at" => "2023-01-01T00:00:00Z"
|
|
334
|
-
+ }],
|
|
335
|
-
+ "rules" => [{
|
|
336
|
-
+ "id" => "rule_override", "flag_id" => "flag1", "account_id" => "acc_1",
|
|
337
|
-
+ "environment_id" => "env_1", "name" => "Company Override",
|
|
338
|
-
+ "rule_type" => "company_override", "priority" => 0, "value" => true,
|
|
339
|
-
+ "condition_groups" => [],
|
|
340
|
-
+ "conditions" => [
|
|
341
|
-
+ {
|
|
342
|
-
+ "id" => "cond_company", "account_id" => "acc_1", "environment_id" => "env_1",
|
|
343
|
-
+ "condition_type" => "company", "operator" => "eq",
|
|
344
|
-
+ "resource_ids" => ["co_entitled"], "trait_value" => ""
|
|
345
|
-
+ },
|
|
346
|
-
+ {
|
|
347
|
-
+ "id" => "cond_metric", "account_id" => "acc_1", "environment_id" => "env_1",
|
|
348
|
-
+ "condition_type" => "metric", "operator" => "lt", "event_subtype" => "api-calls",
|
|
349
|
-
+ "metric_value" => 100, "metric_period" => "current_month",
|
|
350
|
-
+ "metric_period_month_reset" => "billing_cycle", "trait_value" => "100"
|
|
351
|
-
+ }
|
|
352
|
-
+ ]
|
|
353
|
-
+ }]
|
|
354
|
-
+ }
|
|
355
|
-
+ end
|
|
356
|
-
+
|
|
357
|
-
+ def test_billing_metric_override_evaluates_without_trapping
|
|
358
|
-
+ result = @engine.check_flag(flag, entitled_company)
|
|
359
|
-
+
|
|
360
|
-
+ assert result[:value], "company override should grant the flag, not fall back to default false"
|
|
361
|
-
+ assert_match(/override/i, result[:reason].to_s)
|
|
362
|
-
+ end
|
|
363
|
-
+
|
|
364
|
-
+ def test_billing_metric_override_populates_reset_at
|
|
365
|
-
+ result = @engine.check_flag(flag, entitled_company)
|
|
366
|
-
+
|
|
367
|
-
+ refute_nil result[:feature_usage_reset_at],
|
|
368
|
-
+ "reset-at should be computed from the injected host time"
|
|
369
|
-
+ end
|
|
370
|
-
+end
|
|
371
|
-
theirs_snapshot:
|
|
372
|
-
test/rules_engine_clock_test.rb: |
|
|
373
|
-
# frozen_string_literal: true
|
|
374
|
-
|
|
375
|
-
require_relative "test_helper"
|
|
376
|
-
require "minitest/autorun"
|
|
377
|
-
|
|
378
|
-
# Regression for SCHY-471.
|
|
379
|
-
#
|
|
380
|
-
# A company-override entitlement rule whose metric condition uses a
|
|
381
|
-
# calendar/billing metric period drives the engine into the metric-period-reset
|
|
382
|
-
# code path, which needs a wall clock. On the raw wasm32-unknown-unknown build
|
|
383
|
-
# that path used to trap (+wasm unreachable+); the flag then wrongly fell back to
|
|
384
|
-
# its default value for a company that is legitimately entitled. The host now
|
|
385
|
-
# injects the current time via +setCurrentTimeMillis+ so this evaluates cleanly.
|
|
386
|
-
class RulesEngineClockTest < Minitest::Test
|
|
387
|
-
def setup
|
|
388
|
-
@engine = Schematic::RulesEngine.new
|
|
389
|
-
@engine.initialize!
|
|
390
|
-
# The wasm binary is fetched via scripts/download-wasm.sh and may be absent
|
|
391
|
-
# (or the wasmtime gem missing) in some environments — skip rather than fail.
|
|
392
|
-
skip "WASM rules engine unavailable (wasm binary or wasmtime gem missing)" unless @engine.initialized?
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
def flag
|
|
396
|
-
{
|
|
397
|
-
"id" => "flag1", "key" => "mcp-access", "account_id" => "acc_1",
|
|
398
|
-
"environment_id" => "env_1", "default_value" => false, "rules" => []
|
|
399
|
-
}
|
|
400
|
-
end
|
|
401
|
-
|
|
402
|
-
# Usage 40 < allocation 100, so the override grants the feature.
|
|
403
|
-
def entitled_company
|
|
404
|
-
{
|
|
405
|
-
"id" => "co_entitled", "account_id" => "acc_1", "environment_id" => "env_1",
|
|
406
|
-
"keys" => { "id" => "co_entitled" },
|
|
407
|
-
"metrics" => [{
|
|
408
|
-
"account_id" => "acc_1", "environment_id" => "env_1", "company_id" => "co_entitled",
|
|
409
|
-
"event_subtype" => "api-calls", "period" => "current_month",
|
|
410
|
-
"month_reset" => "billing_cycle", "value" => 40, "created_at" => "2023-01-01T00:00:00Z"
|
|
411
|
-
}],
|
|
412
|
-
"rules" => [{
|
|
413
|
-
"id" => "rule_override", "flag_id" => "flag1", "account_id" => "acc_1",
|
|
414
|
-
"environment_id" => "env_1", "name" => "Company Override",
|
|
415
|
-
"rule_type" => "company_override", "priority" => 0, "value" => true,
|
|
416
|
-
"condition_groups" => [],
|
|
417
|
-
"conditions" => [
|
|
418
|
-
{
|
|
419
|
-
"id" => "cond_company", "account_id" => "acc_1", "environment_id" => "env_1",
|
|
420
|
-
"condition_type" => "company", "operator" => "eq",
|
|
421
|
-
"resource_ids" => ["co_entitled"], "trait_value" => ""
|
|
422
|
-
},
|
|
423
|
-
{
|
|
424
|
-
"id" => "cond_metric", "account_id" => "acc_1", "environment_id" => "env_1",
|
|
425
|
-
"condition_type" => "metric", "operator" => "lt", "event_subtype" => "api-calls",
|
|
426
|
-
"metric_value" => 100, "metric_period" => "current_month",
|
|
427
|
-
"metric_period_month_reset" => "billing_cycle", "trait_value" => "100"
|
|
428
|
-
}
|
|
429
|
-
]
|
|
430
|
-
}]
|
|
431
|
-
}
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
def test_billing_metric_override_evaluates_without_trapping
|
|
435
|
-
result = @engine.check_flag(flag, entitled_company)
|
|
436
|
-
|
|
437
|
-
assert result[:value], "company override should grant the flag, not fall back to default false"
|
|
438
|
-
assert_match(/override/i, result[:reason].to_s)
|
|
439
|
-
end
|
|
440
|
-
|
|
441
|
-
def test_billing_metric_override_populates_reset_at
|
|
442
|
-
result = @engine.check_flag(flag, entitled_company)
|
|
443
|
-
|
|
444
|
-
refute_nil result[:feature_usage_reset_at],
|
|
445
|
-
"reset-at should be computed from the injected host time"
|
|
446
|
-
end
|
|
447
|
-
end
|
|
448
|
-
user_owned: true
|
|
111
|
+
- commit_sha: ec0eabecc3325dfe0a69590f9a2f4e52c3c18ade
|
|
112
|
+
tree_hash: 10ac19e0d7c216b0cf04c6620979634798d2ca8a
|
|
113
|
+
timestamp: 2026-09-28T10:03:35.278Z
|
|
114
|
+
cli_version: unknown
|
|
115
|
+
generator_versions:
|
|
116
|
+
fernapi/fern-ruby-sdk: 1.25.0
|
|
117
|
+
- commit_sha: eae816455d05776057b4d684b5cd4effe47ef299
|
|
118
|
+
tree_hash: 3af6229006f1467db74835ba62abc1dcff846262
|
|
119
|
+
timestamp: 2026-10-05T10:05:38.729Z
|
|
120
|
+
cli_version: unknown
|
|
121
|
+
generator_versions:
|
|
122
|
+
fernapi/fern-ruby-sdk: 1.25.0
|
|
123
|
+
current_generation: eae816455d05776057b4d684b5cd4effe47ef299
|
|
124
|
+
patches: []
|
data/.fernignore
CHANGED
|
@@ -6,8 +6,10 @@ CONTRIBUTING.md
|
|
|
6
6
|
LICENSE
|
|
7
7
|
README.md
|
|
8
8
|
WASM_VERSION
|
|
9
|
+
conformance/
|
|
9
10
|
custom.gemspec.rb
|
|
10
11
|
lib/schematic/cache.rb
|
|
12
|
+
lib/schematic/credits/leases/
|
|
11
13
|
lib/schematic/datastream/
|
|
12
14
|
lib/schematic/event_buffer.rb
|
|
13
15
|
lib/schematic/logger.rb
|
|
@@ -18,7 +20,11 @@ lib/schematic/wasm/
|
|
|
18
20
|
lib/schematic/webhook_verification.rb
|
|
19
21
|
lib/schematichq.rb
|
|
20
22
|
scripts/
|
|
23
|
+
test/conformance_test.rb
|
|
24
|
+
test/credits_test.rb
|
|
21
25
|
test/custom.test.rb
|
|
26
|
+
test/lease_support.rb
|
|
27
|
+
test/rules_engine_clock_test.rb
|
|
22
28
|
testapp/
|
|
23
29
|
.fern/replay.lock
|
|
24
30
|
.fern/replay.yml
|
data/README.md
CHANGED
|
@@ -363,6 +363,147 @@ end
|
|
|
363
363
|
client.close
|
|
364
364
|
```
|
|
365
365
|
|
|
366
|
+
### Credit Leases and Reservations
|
|
367
|
+
|
|
368
|
+
For features metered by credit burndown (e.g. inference tokens), `check` reserves credits for the work about to run and `track_with_reservation` settles the reservation with actual usage. The SDK gates in one of two modes:
|
|
369
|
+
|
|
370
|
+
- **Client mode** acquires a **lease**, a tranche of credits held against the company's balance, and carves a per-request **reservation** out of it locally, so a check needs no API call. It requires [DataStream](#datastream) (or [Replicator Mode](#replicator-mode)) and, across multiple processes, a shared Redis so every instance gates against the same lease.
|
|
371
|
+
- **Server mode** makes one `check-and-reserve` API call per check. No lease, no Redis, no local state.
|
|
372
|
+
|
|
373
|
+
`credit_leases[:mode]` defaults to `:auto`: client when DataStream is enabled, server otherwise. Client mode suits high-throughput gating; server mode suits low-volume checks and operations that run for seconds.
|
|
374
|
+
|
|
375
|
+
Durations are in milliseconds, as in the other Schematic SDKs.
|
|
376
|
+
|
|
377
|
+
#### Setup
|
|
378
|
+
|
|
379
|
+
The `redis` gem is not a dependency; pass a client you already have.
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
require "redis"
|
|
383
|
+
require "schematichq"
|
|
384
|
+
|
|
385
|
+
redis_client = Redis.new(url: "redis://localhost:6379")
|
|
386
|
+
|
|
387
|
+
client = Schematic::SchematicClient.new(
|
|
388
|
+
api_key: ENV["SCHEMATIC_API_KEY"],
|
|
389
|
+
use_data_stream: true,
|
|
390
|
+
datastream_options: {
|
|
391
|
+
redis_client: redis_client # also backs lease and reservation state
|
|
392
|
+
},
|
|
393
|
+
credit_leases: {
|
|
394
|
+
default_lease_size: 10_000, # credits requested per lease
|
|
395
|
+
default_lease_duration: 5 * 60 * 1000, # lease lifetime (ms)
|
|
396
|
+
default_reservation_ttl: 60 * 1000 # how long a reservation is held if no track settles it (ms)
|
|
397
|
+
}
|
|
398
|
+
)
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Set `credit_leases[:redis_client]` to keep lease state in a different Redis than the DataStream cache.
|
|
402
|
+
|
|
403
|
+
Server mode needs only a TTL:
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
client = Schematic::SchematicClient.new(
|
|
407
|
+
api_key: ENV["SCHEMATIC_API_KEY"],
|
|
408
|
+
credit_leases: {
|
|
409
|
+
default_reservation_ttl: 60 * 1000 # how long the server holds the credits if no track settles them (ms, max 1 hour)
|
|
410
|
+
}
|
|
411
|
+
)
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Only `mode` and `default_reservation_ttl` apply in server mode; the SDK warns at startup if a client-only option is set.
|
|
415
|
+
|
|
416
|
+
#### Checking and tracking
|
|
417
|
+
|
|
418
|
+
```ruby
|
|
419
|
+
# Reserve up to max_tokens for this operation.
|
|
420
|
+
result = client.check(
|
|
421
|
+
"inference",
|
|
422
|
+
company: { "id" => "your-company-id" },
|
|
423
|
+
usage: max_tokens, # upper bound for this operation
|
|
424
|
+
event_subtype: "inference_tokens" # the metered event
|
|
425
|
+
)
|
|
426
|
+
raise "credit balance exceeded" unless result.allowed?
|
|
427
|
+
|
|
428
|
+
inference = run_inference(...)
|
|
429
|
+
|
|
430
|
+
# Report actual usage; the unused slice of the reservation is refunded.
|
|
431
|
+
if result.reservation
|
|
432
|
+
client.track_with_reservation(result.reservation, inference.tokens_used)
|
|
433
|
+
else
|
|
434
|
+
client.track({
|
|
435
|
+
event: "inference_tokens",
|
|
436
|
+
company: { "id" => "your-company-id" },
|
|
437
|
+
quantity: inference.tokens_used
|
|
438
|
+
})
|
|
439
|
+
end
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
A check can allow without reserving credits (the feature is not credit-metered, `usage` is 0, or the check failed open), and that usage still has to be tracked.
|
|
443
|
+
|
|
444
|
+
Without `credit_leases` configured, or without a `usage`, `check` falls through to a plain flag check with no reservation. `usage` is still sent as a preflight, locally or to the API, so the verdict accounts for what the call is about to spend. Preflighted verdicts are not cached.
|
|
445
|
+
|
|
446
|
+
`usage` may be fractional. A client-mode reservation is sized in whole event units, `ceil(usage) x consumption_rate`, and a settle debits the lease by `ceil(actual) x consumption_rate`, so the local ledger moves by exactly what the track event bills. The reservation still records the fractional quantity the caller declared. Everything on the wire rounds up with the debit: the track event's quantity, a server-mode reservation, and the preflight quantity, whether it goes to the API or the local engine.
|
|
447
|
+
|
|
448
|
+
`result.entitlement` is a symbol-keyed hash with the field names of `Schematic::Types::FeatureEntitlement`, the same in both modes.
|
|
449
|
+
|
|
450
|
+
An unsettled reservation expires after `default_reservation_ttl` and its credits return to the lease. A late settle still bills the usage (the track event carries a deterministic idempotency key, so it never double-bills) but does not re-debit the local lease, so set `default_reservation_ttl` above the longest expected gap between `check` and `track_with_reservation`.
|
|
451
|
+
|
|
452
|
+
#### Pre-warming
|
|
453
|
+
|
|
454
|
+
Pre-warm leases when the user is identified, so a session's first check does not wait on a lease acquire:
|
|
455
|
+
|
|
456
|
+
```ruby
|
|
457
|
+
client.identify(
|
|
458
|
+
{
|
|
459
|
+
keys: { "user_id" => "your-user-id" },
|
|
460
|
+
company: { keys: { "id" => "your-company-id" } }
|
|
461
|
+
},
|
|
462
|
+
prewarm: ["credit-type-id"] # credit type IDs to acquire leases for
|
|
463
|
+
)
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Or call `client.prewarm(credit_type_ids, company:)` directly. Both are no-ops in server mode.
|
|
467
|
+
|
|
468
|
+
Pre-warming resolves the company the way the server does: it looks the keys up first, whatever they are named, and only when nothing matches does it read a value carrying Schematic's `comp_` prefix as the company id.
|
|
469
|
+
|
|
470
|
+
#### Failure behavior
|
|
471
|
+
|
|
472
|
+
A check that cannot be gated (API unreachable, Redis down, lease exhausted) fails closed by default. Override per check:
|
|
473
|
+
|
|
474
|
+
```ruby
|
|
475
|
+
result = client.check(
|
|
476
|
+
"inference",
|
|
477
|
+
company: { "id" => "your-company-id" },
|
|
478
|
+
usage: max_tokens,
|
|
479
|
+
event_subtype: "inference_tokens",
|
|
480
|
+
on_acquire_failure: :fail_open
|
|
481
|
+
)
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
In server mode, a check that times out after the server has already reserved leaves those credits reserved until the TTL expires, so keep `default_reservation_ttl` short there.
|
|
485
|
+
|
|
486
|
+
In client mode, `:fail_open` still evaluates the flag's rules with the credit balance assumed sufficient, so plan targeting and all non-credit conditions apply and only the credit gate is bypassed. In server mode it returns the flag's default value, which is `false` unless you pass `default_value` or configure a `flag_defaults` entry.
|
|
487
|
+
|
|
488
|
+
`default_value` also applies when a check falls back to a plain flag check and that check fails. Pass a boolean or a callable.
|
|
489
|
+
|
|
490
|
+
`check` accepts `timeout_ms`, but no timeout is configurable yet: the generated HTTP transport ignores per-request timeouts and the client exposes no setting. The option is carried on the request so it takes effect once the transport reads it.
|
|
491
|
+
|
|
492
|
+
#### Configuration options
|
|
493
|
+
|
|
494
|
+
| Option | Type | Default | Description |
|
|
495
|
+
|---|---|---|---|
|
|
496
|
+
| `mode` | `:client`, `:server`, `:auto` | `:auto` | Where credits are reserved; `:auto` picks client when DataStream is enabled, server otherwise |
|
|
497
|
+
| `default_reservation_ttl` | `Integer` | 60000 (60 seconds) | How long an unsettled reservation is held (ms) |
|
|
498
|
+
| `default_lease_duration` | `Integer` | 300000 (5 minutes) | (client mode) Lease lifetime (ms) |
|
|
499
|
+
| `default_lease_size` | `Numeric` | 10000 | (client mode) Credits requested per lease acquire or extend |
|
|
500
|
+
| `low_water_mark` | `Float` | 0.25 | (client mode) Extend in the background when the lease balance dips below this fraction |
|
|
501
|
+
| `sweep_interval_ms` | `Integer` | 1000 | (client mode) Sweep interval for expired reservations (ms) |
|
|
502
|
+
| `prewarm_resolve_timeout_ms` | `Integer` | 5000 | (client mode) How long `prewarm` waits for a freshly identified company to surface (ms); 0 resolves from the DataStream cache only |
|
|
503
|
+
| `redis_client` | Redis client | `datastream_options[:redis_client]` | (client mode) Redis client for lease and reservation state |
|
|
504
|
+
| `redis_key_prefix` | `String` | `datastream_options[:redis_key_prefix]` | (client mode) Key prefix for lease and reservation keys |
|
|
505
|
+
| `overrides` | `Hash` | none | (client mode) Per-credit-type overrides of the above, keyed by credit type ID |
|
|
506
|
+
|
|
366
507
|
### Other API operations
|
|
367
508
|
|
|
368
509
|
The Schematic API supports many operations beyond these, accessible via the API modules on the client: `accounts`, `billing`, `companies`, `credits`, `entitlements`, `events`, `features`, and `plans`.
|
data/WASM_VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.9.0
|
data/custom.gemspec.rb
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
def add_custom_gemspec_data(spec)
|
|
4
4
|
spec.homepage = "https://github.com/SchematicHQ/schematic-ruby"
|
|
5
5
|
|
|
6
|
+
# Cross-SDK test fixtures, read only by test/conformance_test.rb, which the
|
|
7
|
+
# generated gemspec already drops with the rest of test/. Rejected here rather
|
|
8
|
+
# than there because that file is generated; npm's allowlist keeps them out of
|
|
9
|
+
# schematic-node the same way.
|
|
10
|
+
spec.files.reject! { |f| f.start_with?("conformance/") }
|
|
11
|
+
|
|
6
12
|
# The WASM binary is gitignored (downloaded at build time via scripts/download-wasm.sh)
|
|
7
13
|
# but must be included in the published gem. Append it to the file list.
|
|
8
14
|
wasm_file = "lib/schematic/wasm/rulesengine.wasm"
|
|
@@ -9,6 +9,8 @@ module Schematic
|
|
|
9
9
|
field :environment_type, -> { Schematic::Types::EnvironmentType }, optional: true, nullable: false
|
|
10
10
|
|
|
11
11
|
field :name, -> { String }, optional: true, nullable: false
|
|
12
|
+
|
|
13
|
+
field :require_context_signature, -> { Internal::Types::Boolean }, optional: true, nullable: false
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
end
|
data/lib/schematic/client.rb
CHANGED
|
@@ -42,7 +42,7 @@ module Schematic
|
|
|
42
42
|
@raw_client = Schematic::Internal::Http::RawClient.new(
|
|
43
43
|
base_url: base_url || Schematic::Environment::DEFAULT,
|
|
44
44
|
headers: {
|
|
45
|
-
"User-Agent" => "schematichq/1.5.
|
|
45
|
+
"User-Agent" => "schematichq/1.5.3",
|
|
46
46
|
"X-Fern-Language" => "Ruby",
|
|
47
47
|
"X-Schematic-Api-Key" => api_key.to_s
|
|
48
48
|
},
|