@traffical/sdk-spec 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Traffical
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # Traffical SDK Specification
2
+
3
+ Language-agnostic specifications for implementing Traffical SDKs.
4
+
5
+ ## Overview
6
+
7
+ This repository contains:
8
+
9
+ - **JSON Schemas** - Type definitions for configuration bundles and events
10
+ - **Test Vectors** - Deterministic test fixtures for validating SDK implementations
11
+
12
+ All Traffical SDKs must implement these specifications to ensure consistent behavior across languages.
13
+
14
+ ## Schemas
15
+
16
+ ### `config-bundle.schema.json`
17
+
18
+ The complete configuration bundle that SDKs fetch and cache. Contains:
19
+
20
+ - Organization and project identifiers
21
+ - Hashing configuration (unit key, bucket count)
22
+ - Parameters with defaults and layer membership
23
+ - Layers with policies and allocations
24
+ - Conditions for targeting
25
+
26
+ ### `events.schema.json`
27
+
28
+ Event payloads sent to the Traffical API:
29
+
30
+ - Exposure events (when a user is assigned to a variant)
31
+ - Track events (custom user actions)
32
+
33
+ ### `traffical-config.schema.json`
34
+
35
+ Local project configuration file (`.traffical/config.yaml`) schema.
36
+
37
+ ## Test Vectors
38
+
39
+ The `test-vectors/` directory contains deterministic test fixtures for validating SDK implementations.
40
+
41
+ ### Purpose
42
+
43
+ All Traffical SDKs must produce identical results for the same inputs:
44
+
45
+ 1. **Hashing consistency** - FNV-1a hash produces the same bucket across all implementations
46
+ 2. **Resolution correctness** - Parameter resolution follows the layered priority system
47
+ 3. **Condition evaluation** - Context predicates are evaluated identically
48
+
49
+ ### Fixture Structure
50
+
51
+ ```
52
+ test-vectors/
53
+ ├── fixtures/
54
+ │ ├── bundle_*.json # Config bundles to test against
55
+ │ └── expected_*.json # Expected outputs for each bundle
56
+ └── README.md
57
+ ```
58
+
59
+ ### Running Tests
60
+
61
+ Each SDK implementation should:
62
+
63
+ 1. Load the bundle JSON
64
+ 2. For each test case:
65
+ - Compute buckets and verify against `expectedHashing`
66
+ - Resolve parameters and verify against `expectedAssignments`
67
+
68
+ ## Hash Function Reference
69
+
70
+ Traffical uses **FNV-1a (32-bit)** for bucket computation:
71
+
72
+ ```
73
+ Input: unitKeyValue + ":" + layerId
74
+ Hash: FNV-1a(input) >>> 0
75
+ Bucket: hash % bucketCount
76
+ ```
77
+
78
+ ### Example
79
+
80
+ - Input: `"user-abc:layer_ui"`
81
+ - FNV-1a hash: `2947556742`
82
+ - Bucket (mod 1000): `742`
83
+
84
+ ### FNV-1a Algorithm
85
+
86
+ ```
87
+ FNV_OFFSET_BASIS = 2166136261
88
+ FNV_PRIME = 16777619
89
+
90
+ hash = FNV_OFFSET_BASIS
91
+ for each byte in input:
92
+ hash = hash XOR byte
93
+ hash = hash * FNV_PRIME
94
+ return hash >>> 0 // Ensure unsigned 32-bit
95
+ ```
96
+
97
+ ## SDK Implementations
98
+
99
+ | Language | Repository |
100
+ |----------|------------|
101
+ | JavaScript/TypeScript | [traffical/js-sdk](https://github.com/traffical/js-sdk) |
102
+ | Go | Coming soon |
103
+ | Java | Coming soon |
104
+
105
+ ## License
106
+
107
+ MIT License - see [LICENSE](LICENSE) for details.
108
+
package/index.cjs ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @traffical/sdk-spec (CommonJS)
3
+ *
4
+ * Language-agnostic specifications for Traffical SDKs.
5
+ * Provides JSON schemas and test vectors for validation.
6
+ */
7
+
8
+ const path = require('path');
9
+
10
+ // Schemas
11
+ exports.configBundleSchema = require('./schemas/config-bundle.schema.json');
12
+ exports.eventsSchema = require('./schemas/events.schema.json');
13
+ exports.trafficalConfigSchema = require('./schemas/traffical-config.schema.json');
14
+
15
+ // Test vector bundles
16
+ exports.bundleBasic = require('./test-vectors/fixtures/bundle_basic.json');
17
+ exports.bundleConditions = require('./test-vectors/fixtures/bundle_conditions.json');
18
+
19
+ // Test vector expected results
20
+ exports.expectedBasic = require('./test-vectors/fixtures/expected_basic.json');
21
+ exports.expectedConditions = require('./test-vectors/fixtures/expected_conditions.json');
22
+
23
+ // Schema paths (for tools that need file paths)
24
+ exports.schemaPaths = {
25
+ configBundle: path.join(__dirname, 'schemas/config-bundle.schema.json'),
26
+ events: path.join(__dirname, 'schemas/events.schema.json'),
27
+ trafficalConfig: path.join(__dirname, 'schemas/traffical-config.schema.json'),
28
+ };
29
+
30
+ // Test vector paths
31
+ exports.testVectorPaths = {
32
+ bundleBasic: path.join(__dirname, 'test-vectors/fixtures/bundle_basic.json'),
33
+ bundleConditions: path.join(__dirname, 'test-vectors/fixtures/bundle_conditions.json'),
34
+ expectedBasic: path.join(__dirname, 'test-vectors/fixtures/expected_basic.json'),
35
+ expectedConditions: path.join(__dirname, 'test-vectors/fixtures/expected_conditions.json'),
36
+ };
37
+
package/index.d.ts ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @traffical/sdk-spec
3
+ *
4
+ * Type definitions for Traffical SDK specifications.
5
+ */
6
+
7
+ import type { JSONSchema7 } from 'json-schema';
8
+
9
+ // Schema types
10
+ export declare const configBundleSchema: JSONSchema7;
11
+ export declare const eventsSchema: JSONSchema7;
12
+ export declare const trafficalConfigSchema: JSONSchema7;
13
+
14
+ // Test vector bundle types
15
+ export interface TestBundle {
16
+ orgId: string;
17
+ projectId: string;
18
+ env: string;
19
+ version: number;
20
+ hashing: {
21
+ unitKey: string;
22
+ bucketCount: number;
23
+ };
24
+ parameters: Record<string, {
25
+ type: string;
26
+ default: unknown;
27
+ layers?: string[];
28
+ }>;
29
+ layers: Array<{
30
+ id: string;
31
+ priority: number;
32
+ policies: Array<{
33
+ id: string;
34
+ parameters: Record<string, unknown>;
35
+ allocations: Array<{
36
+ name: string;
37
+ bucketRange: [number, number];
38
+ parameters: Record<string, unknown>;
39
+ }>;
40
+ conditions?: Array<{
41
+ type: string;
42
+ field: string;
43
+ operator: string;
44
+ value: unknown;
45
+ }>;
46
+ }>;
47
+ }>;
48
+ }
49
+
50
+ export interface ExpectedResults {
51
+ description: string;
52
+ testCases: Array<{
53
+ name: string;
54
+ context: Record<string, unknown>;
55
+ expectedHashing?: Record<string, {
56
+ input: string;
57
+ bucket: number;
58
+ }>;
59
+ expectedAssignments: Record<string, unknown>;
60
+ }>;
61
+ }
62
+
63
+ export declare const bundleBasic: TestBundle;
64
+ export declare const bundleConditions: TestBundle;
65
+ export declare const expectedBasic: ExpectedResults;
66
+ export declare const expectedConditions: ExpectedResults;
67
+
68
+ // Path exports
69
+ export declare const schemaPaths: {
70
+ configBundle: string;
71
+ events: string;
72
+ trafficalConfig: string;
73
+ };
74
+
75
+ export declare const testVectorPaths: {
76
+ bundleBasic: string;
77
+ bundleConditions: string;
78
+ expectedBasic: string;
79
+ expectedConditions: string;
80
+ };
81
+
package/index.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @traffical/sdk-spec
3
+ *
4
+ * Language-agnostic specifications for Traffical SDKs.
5
+ * Provides JSON schemas and test vectors for validation.
6
+ */
7
+
8
+ import { createRequire } from 'module';
9
+ const require = createRequire(import.meta.url);
10
+
11
+ // Schemas
12
+ export const configBundleSchema = require('./schemas/config-bundle.schema.json');
13
+ export const eventsSchema = require('./schemas/events.schema.json');
14
+ export const trafficalConfigSchema = require('./schemas/traffical-config.schema.json');
15
+
16
+ // Test vector bundles
17
+ export const bundleBasic = require('./test-vectors/fixtures/bundle_basic.json');
18
+ export const bundleConditions = require('./test-vectors/fixtures/bundle_conditions.json');
19
+
20
+ // Test vector expected results
21
+ export const expectedBasic = require('./test-vectors/fixtures/expected_basic.json');
22
+ export const expectedConditions = require('./test-vectors/fixtures/expected_conditions.json');
23
+
24
+ // Schema paths (for tools that need file paths)
25
+ export const schemaPaths = {
26
+ configBundle: new URL('./schemas/config-bundle.schema.json', import.meta.url).pathname,
27
+ events: new URL('./schemas/events.schema.json', import.meta.url).pathname,
28
+ trafficalConfig: new URL('./schemas/traffical-config.schema.json', import.meta.url).pathname,
29
+ };
30
+
31
+ // Test vector paths
32
+ export const testVectorPaths = {
33
+ bundleBasic: new URL('./test-vectors/fixtures/bundle_basic.json', import.meta.url).pathname,
34
+ bundleConditions: new URL('./test-vectors/fixtures/bundle_conditions.json', import.meta.url).pathname,
35
+ expectedBasic: new URL('./test-vectors/fixtures/expected_basic.json', import.meta.url).pathname,
36
+ expectedConditions: new URL('./test-vectors/fixtures/expected_conditions.json', import.meta.url).pathname,
37
+ };
38
+
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@traffical/sdk-spec",
3
+ "version": "0.1.0",
4
+ "description": "Language-agnostic specifications for Traffical SDKs - schemas and test vectors",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "types": "./index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./index.d.ts",
11
+ "import": "./index.js",
12
+ "require": "./index.cjs"
13
+ },
14
+ "./schemas/*": "./schemas/*",
15
+ "./test-vectors/*": "./test-vectors/*"
16
+ },
17
+ "files": [
18
+ "schemas",
19
+ "test-vectors",
20
+ "index.js",
21
+ "index.cjs",
22
+ "index.d.ts"
23
+ ],
24
+ "scripts": {},
25
+ "keywords": [
26
+ "traffical",
27
+ "sdk",
28
+ "specification",
29
+ "schema",
30
+ "test-vectors",
31
+ "feature-flags",
32
+ "experimentation"
33
+ ],
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/traffical/sdk-spec"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }
43
+
@@ -0,0 +1,208 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://traffical.io/schemas/config-bundle.json",
4
+ "title": "ConfigBundle",
5
+ "description": "The complete configuration bundle for a Traffical project/environment. This is what the SDK fetches and caches.",
6
+ "type": "object",
7
+ "required": ["version", "orgId", "projectId", "env", "hashing", "parameters", "layers"],
8
+ "properties": {
9
+ "version": {
10
+ "type": "string",
11
+ "format": "date-time",
12
+ "description": "ISO timestamp for cache invalidation / ETag generation"
13
+ },
14
+ "orgId": {
15
+ "type": "string",
16
+ "description": "Organization ID"
17
+ },
18
+ "projectId": {
19
+ "type": "string",
20
+ "description": "Project ID"
21
+ },
22
+ "env": {
23
+ "type": "string",
24
+ "description": "Environment (e.g., 'production', 'staging')"
25
+ },
26
+ "hashing": {
27
+ "$ref": "#/definitions/BundleHashingConfig"
28
+ },
29
+ "parameters": {
30
+ "type": "array",
31
+ "items": {
32
+ "$ref": "#/definitions/BundleParameter"
33
+ },
34
+ "description": "All parameters for this project/env with defaults and layer membership"
35
+ },
36
+ "layers": {
37
+ "type": "array",
38
+ "items": {
39
+ "$ref": "#/definitions/BundleLayer"
40
+ },
41
+ "description": "All layers with their policies"
42
+ }
43
+ },
44
+ "definitions": {
45
+ "BundleHashingConfig": {
46
+ "type": "object",
47
+ "required": ["unitKey", "bucketCount"],
48
+ "properties": {
49
+ "unitKey": {
50
+ "type": "string",
51
+ "description": "The context field name to use as the unit key (e.g., 'userId', 'deviceId')"
52
+ },
53
+ "bucketCount": {
54
+ "type": "integer",
55
+ "minimum": 1,
56
+ "description": "Total number of buckets for allocation (e.g., 1000, 10000)"
57
+ }
58
+ }
59
+ },
60
+ "BundleParameter": {
61
+ "type": "object",
62
+ "required": ["key", "type", "default", "layerId", "namespace"],
63
+ "properties": {
64
+ "key": {
65
+ "type": "string",
66
+ "description": "Parameter key (e.g., 'ui.primaryColor', 'pricing.discount')"
67
+ },
68
+ "type": {
69
+ "type": "string",
70
+ "enum": ["string", "number", "boolean", "json"],
71
+ "description": "Value type"
72
+ },
73
+ "default": {
74
+ "description": "Default value when no policy overrides apply"
75
+ },
76
+ "layerId": {
77
+ "type": "string",
78
+ "description": "The layer this parameter belongs to"
79
+ },
80
+ "namespace": {
81
+ "type": "string",
82
+ "description": "Namespace for organizational purposes"
83
+ }
84
+ }
85
+ },
86
+ "BundleLayer": {
87
+ "type": "object",
88
+ "required": ["id", "policies"],
89
+ "properties": {
90
+ "id": {
91
+ "type": "string",
92
+ "description": "Layer ID"
93
+ },
94
+ "policies": {
95
+ "type": "array",
96
+ "items": {
97
+ "$ref": "#/definitions/BundlePolicy"
98
+ },
99
+ "description": "Policies within this layer (evaluated in order)"
100
+ }
101
+ }
102
+ },
103
+ "BundleContextLogging": {
104
+ "type": "object",
105
+ "required": ["allowedFields"],
106
+ "properties": {
107
+ "allowedFields": {
108
+ "type": "array",
109
+ "items": {
110
+ "type": "string"
111
+ },
112
+ "description": "Context fields to include in exposure events (allowlist). Only these fields will be logged for contextual bandit training."
113
+ }
114
+ },
115
+ "description": "Configuration for context logging in exposure events. Used for contextual bandit training while protecting PII."
116
+ },
117
+ "BundlePolicy": {
118
+ "type": "object",
119
+ "required": ["id", "state", "kind", "allocations", "conditions"],
120
+ "properties": {
121
+ "id": {
122
+ "type": "string",
123
+ "description": "Policy ID for tracking and analytics"
124
+ },
125
+ "state": {
126
+ "type": "string",
127
+ "enum": ["draft", "running", "paused", "completed"],
128
+ "description": "Current state of the policy"
129
+ },
130
+ "kind": {
131
+ "type": "string",
132
+ "enum": ["static", "adaptive"],
133
+ "description": "Policy kind: 'static' for fixed allocations, 'adaptive' for learning-based"
134
+ },
135
+ "allocations": {
136
+ "type": "array",
137
+ "items": {
138
+ "$ref": "#/definitions/BundleAllocation"
139
+ },
140
+ "description": "Bucket ranges mapped to parameter overrides"
141
+ },
142
+ "conditions": {
143
+ "type": "array",
144
+ "items": {
145
+ "$ref": "#/definitions/BundleCondition"
146
+ },
147
+ "description": "Context predicates that must all match for eligibility"
148
+ },
149
+ "stateVersion": {
150
+ "type": "string",
151
+ "format": "date-time",
152
+ "description": "For adaptive policies: version of the optimization state"
153
+ },
154
+ "contextLogging": {
155
+ "$ref": "#/definitions/BundleContextLogging",
156
+ "description": "For adaptive policies: context fields to log in exposure events for contextual bandit training"
157
+ }
158
+ }
159
+ },
160
+ "BundleAllocation": {
161
+ "type": "object",
162
+ "required": ["name", "bucketRange", "overrides"],
163
+ "properties": {
164
+ "name": {
165
+ "type": "string",
166
+ "description": "Variant name for tracking (e.g., 'control', 'treatment_a')"
167
+ },
168
+ "bucketRange": {
169
+ "type": "array",
170
+ "items": {
171
+ "type": "integer"
172
+ },
173
+ "minItems": 2,
174
+ "maxItems": 2,
175
+ "description": "Bucket range [start, end] inclusive"
176
+ },
177
+ "overrides": {
178
+ "type": "object",
179
+ "additionalProperties": true,
180
+ "description": "Parameter overrides for units in this bucket range"
181
+ }
182
+ }
183
+ },
184
+ "BundleCondition": {
185
+ "type": "object",
186
+ "required": ["field", "op"],
187
+ "properties": {
188
+ "field": {
189
+ "type": "string",
190
+ "description": "Context field to evaluate"
191
+ },
192
+ "op": {
193
+ "type": "string",
194
+ "enum": ["eq", "neq", "in", "nin", "gt", "gte", "lt", "lte", "contains", "startsWith", "endsWith", "regex", "exists", "notExists"],
195
+ "description": "Comparison operator"
196
+ },
197
+ "value": {
198
+ "description": "Single value for binary operators"
199
+ },
200
+ "values": {
201
+ "type": "array",
202
+ "description": "Multiple values for 'in'/'nin' operators"
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+
@@ -0,0 +1,239 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://traffical.io/schemas/events.json",
4
+ "title": "TrafficalEvents",
5
+ "description": "Event schemas for tracking exposures, decisions, and user events in Traffical",
6
+ "definitions": {
7
+ "BaseEvent": {
8
+ "type": "object",
9
+ "required": ["type", "orgId", "projectId", "env", "unitKey", "timestamp"],
10
+ "properties": {
11
+ "id": {
12
+ "type": "string",
13
+ "description": "Optional client-generated event ID for deduplication and linking"
14
+ },
15
+ "type": {
16
+ "type": "string",
17
+ "enum": ["exposure", "decision", "track"],
18
+ "description": "Event type"
19
+ },
20
+ "orgId": {
21
+ "type": "string",
22
+ "description": "Organization ID"
23
+ },
24
+ "projectId": {
25
+ "type": "string",
26
+ "description": "Project ID"
27
+ },
28
+ "env": {
29
+ "type": "string",
30
+ "description": "Environment"
31
+ },
32
+ "unitKey": {
33
+ "type": "string",
34
+ "description": "Unit key value - the identifier used for bucketing"
35
+ },
36
+ "userId": {
37
+ "type": "string",
38
+ "description": "Optional user identifier"
39
+ },
40
+ "anonymousId": {
41
+ "type": "string",
42
+ "description": "Optional anonymous identifier"
43
+ },
44
+ "sessionId": {
45
+ "type": "string",
46
+ "description": "Optional session identifier"
47
+ },
48
+ "requestId": {
49
+ "type": "string",
50
+ "description": "Optional request identifier"
51
+ },
52
+ "timestamp": {
53
+ "type": "string",
54
+ "format": "date-time",
55
+ "description": "Event timestamp (ISO 8601)"
56
+ },
57
+ "context": {
58
+ "type": "object",
59
+ "additionalProperties": true,
60
+ "description": "Context snapshot at the time of the event"
61
+ },
62
+ "sdkName": {
63
+ "type": "string",
64
+ "description": "SDK that generated this event (e.g., 'js-client', 'node', 'react')"
65
+ },
66
+ "sdkVersion": {
67
+ "type": "string",
68
+ "description": "Version of the SDK that generated this event"
69
+ }
70
+ }
71
+ },
72
+ "ExposureLayerInfo": {
73
+ "type": "object",
74
+ "required": ["layerId", "bucket"],
75
+ "properties": {
76
+ "layerId": {
77
+ "type": "string"
78
+ },
79
+ "bucket": {
80
+ "type": "integer"
81
+ },
82
+ "policyId": {
83
+ "type": "string"
84
+ },
85
+ "allocationName": {
86
+ "type": "string"
87
+ }
88
+ }
89
+ },
90
+ "TrackAttribution": {
91
+ "type": "object",
92
+ "required": ["layerId", "policyId", "allocationName"],
93
+ "properties": {
94
+ "layerId": {
95
+ "type": "string"
96
+ },
97
+ "policyId": {
98
+ "type": "string"
99
+ },
100
+ "allocationName": {
101
+ "type": "string"
102
+ },
103
+ "weight": {
104
+ "type": "number",
105
+ "minimum": 0,
106
+ "maximum": 1,
107
+ "description": "Attribution weight (0.0-1.0)"
108
+ },
109
+ "model": {
110
+ "type": "string",
111
+ "enum": ["first_touch", "last_touch", "linear", "time_decay", "position_based"],
112
+ "description": "Attribution model used"
113
+ }
114
+ }
115
+ },
116
+ "ExposureEvent": {
117
+ "allOf": [
118
+ { "$ref": "#/definitions/BaseEvent" },
119
+ {
120
+ "type": "object",
121
+ "required": ["assignments", "layers"],
122
+ "properties": {
123
+ "type": {
124
+ "const": "exposure"
125
+ },
126
+ "assignments": {
127
+ "type": "object",
128
+ "additionalProperties": true,
129
+ "description": "The parameter assignments that were exposed"
130
+ },
131
+ "layers": {
132
+ "type": "array",
133
+ "items": {
134
+ "$ref": "#/definitions/ExposureLayerInfo"
135
+ },
136
+ "description": "Per-layer resolution metadata"
137
+ },
138
+ "decisionId": {
139
+ "type": "string",
140
+ "description": "Reference to the decision event"
141
+ }
142
+ }
143
+ }
144
+ ]
145
+ },
146
+ "DecisionEvent": {
147
+ "allOf": [
148
+ { "$ref": "#/definitions/BaseEvent" },
149
+ {
150
+ "type": "object",
151
+ "required": ["assignments", "layers"],
152
+ "properties": {
153
+ "type": {
154
+ "const": "decision"
155
+ },
156
+ "requestedParameters": {
157
+ "type": "array",
158
+ "items": {
159
+ "type": "string"
160
+ },
161
+ "description": "Parameters that were requested"
162
+ },
163
+ "assignments": {
164
+ "type": "object",
165
+ "additionalProperties": true,
166
+ "description": "The resolved assignments"
167
+ },
168
+ "layers": {
169
+ "type": "array",
170
+ "items": {
171
+ "$ref": "#/definitions/ExposureLayerInfo"
172
+ },
173
+ "description": "Resolution metadata"
174
+ },
175
+ "latencyMs": {
176
+ "type": "integer",
177
+ "description": "Processing time in milliseconds"
178
+ }
179
+ }
180
+ }
181
+ ]
182
+ },
183
+ "TrackEvent": {
184
+ "allOf": [
185
+ { "$ref": "#/definitions/BaseEvent" },
186
+ {
187
+ "type": "object",
188
+ "required": ["event"],
189
+ "properties": {
190
+ "type": {
191
+ "const": "track"
192
+ },
193
+ "event": {
194
+ "type": "string",
195
+ "description": "Event name (e.g., 'purchase', 'add_to_cart', 'page_view')"
196
+ },
197
+ "decisionId": {
198
+ "type": "string",
199
+ "description": "Reference to the decision event for attribution"
200
+ },
201
+ "value": {
202
+ "type": "number",
203
+ "description": "Primary numeric value for optimization (e.g., revenue amount)"
204
+ },
205
+ "values": {
206
+ "type": "object",
207
+ "additionalProperties": {
208
+ "type": "number"
209
+ },
210
+ "description": "Secondary values for multi-objective optimization"
211
+ },
212
+ "properties": {
213
+ "type": "object",
214
+ "additionalProperties": true,
215
+ "description": "Event properties/metadata (e.g., orderId, itemSku)"
216
+ },
217
+ "attribution": {
218
+ "type": "array",
219
+ "items": {
220
+ "$ref": "#/definitions/TrackAttribution"
221
+ },
222
+ "description": "Attribution chain - which policies/allocations influenced this"
223
+ },
224
+ "eventTimestamp": {
225
+ "type": "string",
226
+ "format": "date-time",
227
+ "description": "For delayed events: the original event timestamp"
228
+ }
229
+ }
230
+ }
231
+ ]
232
+ }
233
+ },
234
+ "oneOf": [
235
+ { "$ref": "#/definitions/ExposureEvent" },
236
+ { "$ref": "#/definitions/DecisionEvent" },
237
+ { "$ref": "#/definitions/TrackEvent" }
238
+ ]
239
+ }
@@ -0,0 +1,187 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://traffical.io/schemas/config.json",
4
+ "title": "TrafficalConfig",
5
+ "description": "Traffical configuration file schema for traffical.yaml",
6
+ "type": "object",
7
+ "required": ["version", "project", "parameters"],
8
+ "properties": {
9
+ "version": {
10
+ "type": "string",
11
+ "enum": ["1.0"],
12
+ "description": "Config file version"
13
+ },
14
+ "project": {
15
+ "type": "object",
16
+ "description": "Project identification",
17
+ "required": ["id", "orgId"],
18
+ "properties": {
19
+ "id": {
20
+ "type": "string",
21
+ "pattern": "^proj_",
22
+ "description": "Project ID from Traffical (starts with proj_)"
23
+ },
24
+ "orgId": {
25
+ "type": "string",
26
+ "pattern": "^org_",
27
+ "description": "Organization ID from Traffical (starts with org_)"
28
+ }
29
+ },
30
+ "additionalProperties": false
31
+ },
32
+ "parameters": {
33
+ "type": "object",
34
+ "description": "Parameters synced from this config file",
35
+ "additionalProperties": {
36
+ "$ref": "#/definitions/parameter"
37
+ }
38
+ },
39
+ "events": {
40
+ "type": "object",
41
+ "description": "Event definitions synced from this config file",
42
+ "additionalProperties": {
43
+ "$ref": "#/definitions/event"
44
+ }
45
+ }
46
+ },
47
+ "additionalProperties": false,
48
+ "definitions": {
49
+ "parameter": {
50
+ "type": "object",
51
+ "description": "Parameter definition",
52
+ "required": ["type", "default"],
53
+ "properties": {
54
+ "type": {
55
+ "type": "string",
56
+ "enum": ["string", "number", "boolean", "json"],
57
+ "description": "The type of value this parameter holds"
58
+ },
59
+ "default": {
60
+ "description": "Default value used when no policy overrides apply"
61
+ },
62
+ "namespace": {
63
+ "type": "string",
64
+ "description": "Organizational grouping for the parameter"
65
+ },
66
+ "description": {
67
+ "type": "string",
68
+ "description": "Human-readable description of the parameter"
69
+ }
70
+ },
71
+ "additionalProperties": false,
72
+ "allOf": [
73
+ {
74
+ "if": {
75
+ "properties": { "type": { "const": "string" } }
76
+ },
77
+ "then": {
78
+ "properties": { "default": { "type": "string" } }
79
+ }
80
+ },
81
+ {
82
+ "if": {
83
+ "properties": { "type": { "const": "number" } }
84
+ },
85
+ "then": {
86
+ "properties": { "default": { "type": "number" } }
87
+ }
88
+ },
89
+ {
90
+ "if": {
91
+ "properties": { "type": { "const": "boolean" } }
92
+ },
93
+ "then": {
94
+ "properties": { "default": { "type": "boolean" } }
95
+ }
96
+ },
97
+ {
98
+ "if": {
99
+ "properties": { "type": { "const": "json" } }
100
+ },
101
+ "then": {
102
+ "properties": {
103
+ "default": {
104
+ "oneOf": [
105
+ { "type": "object" },
106
+ { "type": "array" }
107
+ ]
108
+ }
109
+ }
110
+ }
111
+ }
112
+ ]
113
+ },
114
+ "event": {
115
+ "type": "object",
116
+ "description": "Event definition",
117
+ "required": ["valueType"],
118
+ "properties": {
119
+ "valueType": {
120
+ "type": "string",
121
+ "enum": ["currency", "count", "rate", "boolean"],
122
+ "description": "The type of value this event tracks"
123
+ },
124
+ "unit": {
125
+ "type": "string",
126
+ "description": "Optional unit for the value (e.g., 'USD', 'items', 'percent')"
127
+ },
128
+ "description": {
129
+ "type": "string",
130
+ "description": "Human-readable description of the event"
131
+ }
132
+ },
133
+ "additionalProperties": false
134
+ }
135
+ },
136
+ "examples": [
137
+ {
138
+ "version": "1.0",
139
+ "project": {
140
+ "id": "proj_abc123",
141
+ "orgId": "org_xyz789"
142
+ },
143
+ "parameters": {
144
+ "checkout.button.color": {
145
+ "type": "string",
146
+ "default": "#FF6600",
147
+ "namespace": "checkout",
148
+ "description": "Primary CTA button background color"
149
+ },
150
+ "checkout.new_flow.enabled": {
151
+ "type": "boolean",
152
+ "default": false,
153
+ "namespace": "checkout"
154
+ },
155
+ "pricing.discount.percentage": {
156
+ "type": "number",
157
+ "default": 0,
158
+ "namespace": "pricing"
159
+ },
160
+ "ui.theme.config": {
161
+ "type": "json",
162
+ "default": {
163
+ "primaryColor": "#FF6600",
164
+ "borderRadius": 8
165
+ },
166
+ "namespace": "ui"
167
+ }
168
+ },
169
+ "events": {
170
+ "purchase": {
171
+ "valueType": "currency",
172
+ "unit": "USD",
173
+ "description": "User completes a purchase"
174
+ },
175
+ "add_to_cart": {
176
+ "valueType": "count",
177
+ "description": "User adds item to cart"
178
+ },
179
+ "checkout_started": {
180
+ "valueType": "boolean",
181
+ "description": "User initiates checkout"
182
+ }
183
+ }
184
+ }
185
+ ]
186
+ }
187
+
@@ -0,0 +1,77 @@
1
+ # Traffical SDK Test Vectors
2
+
3
+ This directory contains deterministic test fixtures for validating SDK implementations across languages.
4
+
5
+ ## Purpose
6
+
7
+ All Traffical SDKs must produce identical results for the same inputs. These test vectors ensure:
8
+
9
+ 1. **Hashing consistency** - FNV-1a hash produces the same bucket across all implementations
10
+ 2. **Resolution correctness** - Parameter resolution follows the layered priority system
11
+ 3. **Condition evaluation** - Context predicates are evaluated identically
12
+
13
+ ## Fixture Structure
14
+
15
+ ```
16
+ fixtures/
17
+ ├── bundle_*.json # Config bundles to test against
18
+ └── expected_*.json # Expected outputs for each bundle
19
+ ```
20
+
21
+ ### Bundle Files
22
+
23
+ Each bundle file contains a complete `ConfigBundle` with:
24
+ - Hashing configuration
25
+ - Parameters with defaults
26
+ - Layers with policies and allocations
27
+
28
+ ### Expected Output Files
29
+
30
+ Each expected file contains:
31
+ - Reference to the bundle file
32
+ - Array of test cases with:
33
+ - Input context
34
+ - Expected bucket assignments (for hashing validation)
35
+ - Expected parameter assignments
36
+
37
+ ## Running Tests
38
+
39
+ ### TypeScript (Reference Implementation)
40
+
41
+ ```bash
42
+ cd sdk/core-ts
43
+ bun test
44
+ ```
45
+
46
+ ### Other Languages
47
+
48
+ Each SDK implementation should:
49
+
50
+ 1. Load the bundle JSON
51
+ 2. For each test case:
52
+ - Compute buckets and verify against `expectedHashing`
53
+ - Resolve parameters and verify against `expectedAssignments`
54
+
55
+ ## Adding New Test Cases
56
+
57
+ When adding new test vectors:
58
+
59
+ 1. Create or update a bundle file with the configuration to test
60
+ 2. Use the TypeScript SDK to compute expected outputs
61
+ 3. Add test cases with clear comments explaining the expected behavior
62
+
63
+ ## Hash Function Reference
64
+
65
+ Traffical uses FNV-1a (32-bit) for bucket computation:
66
+
67
+ ```
68
+ Input: unitKeyValue + ":" + layerId
69
+ Hash: FNV-1a(input) >>> 0
70
+ Bucket: hash % bucketCount
71
+ ```
72
+
73
+ Example:
74
+ - Input: `"user-abc:layer_ui"`
75
+ - FNV-1a hash: `2947556742`
76
+ - Bucket (mod 1000): `742`
77
+
@@ -0,0 +1,89 @@
1
+ {
2
+ "version": "2024-01-01T00:00:00.000Z",
3
+ "orgId": "org_test",
4
+ "projectId": "proj_test",
5
+ "env": "production",
6
+ "hashing": {
7
+ "unitKey": "userId",
8
+ "bucketCount": 1000
9
+ },
10
+ "parameters": [
11
+ {
12
+ "key": "ui.primaryColor",
13
+ "type": "string",
14
+ "default": "#000000",
15
+ "layerId": "layer_ui",
16
+ "namespace": "ui"
17
+ },
18
+ {
19
+ "key": "ui.buttonText",
20
+ "type": "string",
21
+ "default": "Click Me",
22
+ "layerId": "layer_ui",
23
+ "namespace": "ui"
24
+ },
25
+ {
26
+ "key": "pricing.discount",
27
+ "type": "number",
28
+ "default": 0,
29
+ "layerId": "layer_pricing",
30
+ "namespace": "pricing"
31
+ }
32
+ ],
33
+ "layers": [
34
+ {
35
+ "id": "layer_ui",
36
+ "policies": [
37
+ {
38
+ "id": "policy_color_test",
39
+ "state": "running",
40
+ "kind": "static",
41
+ "allocations": [
42
+ {
43
+ "name": "control",
44
+ "bucketRange": [0, 499],
45
+ "overrides": {
46
+ "ui.primaryColor": "#0000FF"
47
+ }
48
+ },
49
+ {
50
+ "name": "treatment",
51
+ "bucketRange": [500, 999],
52
+ "overrides": {
53
+ "ui.primaryColor": "#FF0000"
54
+ }
55
+ }
56
+ ],
57
+ "conditions": []
58
+ }
59
+ ]
60
+ },
61
+ {
62
+ "id": "layer_pricing",
63
+ "policies": [
64
+ {
65
+ "id": "policy_discount",
66
+ "state": "running",
67
+ "kind": "static",
68
+ "allocations": [
69
+ {
70
+ "name": "discount_10",
71
+ "bucketRange": [0, 299],
72
+ "overrides": {
73
+ "pricing.discount": 10
74
+ }
75
+ },
76
+ {
77
+ "name": "discount_20",
78
+ "bucketRange": [300, 599],
79
+ "overrides": {
80
+ "pricing.discount": 20
81
+ }
82
+ }
83
+ ],
84
+ "conditions": []
85
+ }
86
+ ]
87
+ }
88
+ ]
89
+ }
@@ -0,0 +1,76 @@
1
+ {
2
+ "version": "2024-01-01T00:00:00.000Z",
3
+ "orgId": "org_test",
4
+ "projectId": "proj_test",
5
+ "env": "production",
6
+ "hashing": {
7
+ "unitKey": "userId",
8
+ "bucketCount": 1000
9
+ },
10
+ "parameters": [
11
+ {
12
+ "key": "checkout.ctaText",
13
+ "type": "string",
14
+ "default": "Complete Purchase",
15
+ "layerId": "layer_checkout",
16
+ "namespace": "checkout"
17
+ },
18
+ {
19
+ "key": "checkout.showUrgency",
20
+ "type": "boolean",
21
+ "default": false,
22
+ "layerId": "layer_checkout",
23
+ "namespace": "checkout"
24
+ }
25
+ ],
26
+ "layers": [
27
+ {
28
+ "id": "layer_checkout",
29
+ "policies": [
30
+ {
31
+ "id": "policy_high_value",
32
+ "state": "running",
33
+ "kind": "static",
34
+ "allocations": [
35
+ {
36
+ "name": "urgency_treatment",
37
+ "bucketRange": [0, 999],
38
+ "overrides": {
39
+ "checkout.ctaText": "Buy Now - Limited Stock!",
40
+ "checkout.showUrgency": true
41
+ }
42
+ }
43
+ ],
44
+ "conditions": [
45
+ {
46
+ "field": "cartValue",
47
+ "op": "gte",
48
+ "value": 100
49
+ }
50
+ ]
51
+ },
52
+ {
53
+ "id": "policy_mobile",
54
+ "state": "running",
55
+ "kind": "static",
56
+ "allocations": [
57
+ {
58
+ "name": "mobile_cta",
59
+ "bucketRange": [0, 999],
60
+ "overrides": {
61
+ "checkout.ctaText": "Buy Now"
62
+ }
63
+ }
64
+ ],
65
+ "conditions": [
66
+ {
67
+ "field": "deviceType",
68
+ "op": "eq",
69
+ "value": "mobile"
70
+ }
71
+ ]
72
+ }
73
+ ]
74
+ }
75
+ ]
76
+ }
@@ -0,0 +1,72 @@
1
+ {
2
+ "description": "Test vectors for basic parameter resolution",
3
+ "bundle": "bundle_basic.json",
4
+ "testCases": [
5
+ {
6
+ "name": "user_abc_treatment_group",
7
+ "context": {
8
+ "userId": "user-abc"
9
+ },
10
+ "expectedHashing": {
11
+ "layer_ui": {
12
+ "bucket": 551,
13
+ "comment": "FNV-1a hash of 'user-abc:layer_ui' mod 1000"
14
+ },
15
+ "layer_pricing": {
16
+ "bucket": 913,
17
+ "comment": "FNV-1a hash of 'user-abc:layer_pricing' mod 1000"
18
+ }
19
+ },
20
+ "expectedAssignments": {
21
+ "ui.primaryColor": "#FF0000",
22
+ "ui.buttonText": "Click Me",
23
+ "pricing.discount": 0
24
+ },
25
+ "comment": "User in treatment bucket for UI (551 >= 500), no discount (913 >= 600)"
26
+ },
27
+ {
28
+ "name": "user_xyz_control_group",
29
+ "context": {
30
+ "userId": "user-xyz"
31
+ },
32
+ "expectedHashing": {
33
+ "layer_ui": {
34
+ "bucket": 214,
35
+ "comment": "FNV-1a hash of 'user-xyz:layer_ui' mod 1000"
36
+ },
37
+ "layer_pricing": {
38
+ "bucket": 42,
39
+ "comment": "FNV-1a hash of 'user-xyz:layer_pricing' mod 1000"
40
+ }
41
+ },
42
+ "expectedAssignments": {
43
+ "ui.primaryColor": "#0000FF",
44
+ "ui.buttonText": "Click Me",
45
+ "pricing.discount": 10
46
+ },
47
+ "comment": "User in control bucket for UI (214 < 500), 10% discount (42 in 0-299)"
48
+ },
49
+ {
50
+ "name": "user_123_treatment",
51
+ "context": {
52
+ "userId": "user-123"
53
+ },
54
+ "expectedHashing": {
55
+ "layer_ui": {
56
+ "bucket": 871,
57
+ "comment": "FNV-1a hash of 'user-123:layer_ui' mod 1000"
58
+ },
59
+ "layer_pricing": {
60
+ "bucket": 177,
61
+ "comment": "FNV-1a hash of 'user-123:layer_pricing' mod 1000"
62
+ }
63
+ },
64
+ "expectedAssignments": {
65
+ "ui.primaryColor": "#FF0000",
66
+ "ui.buttonText": "Click Me",
67
+ "pricing.discount": 10
68
+ },
69
+ "comment": "User in treatment bucket for UI (871 >= 500), 10% discount (177 in 0-299)"
70
+ }
71
+ ]
72
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "description": "Test vectors for condition-based targeting",
3
+ "bundle": "bundle_conditions.json",
4
+ "testCases": [
5
+ {
6
+ "name": "high_value_cart",
7
+ "context": {
8
+ "userId": "user-high-value",
9
+ "cartValue": 150,
10
+ "deviceType": "desktop"
11
+ },
12
+ "expectedAssignments": {
13
+ "checkout.ctaText": "Buy Now - Limited Stock!",
14
+ "checkout.showUrgency": true
15
+ },
16
+ "comment": "High value cart triggers urgency treatment"
17
+ },
18
+ {
19
+ "name": "mobile_user_low_cart",
20
+ "context": {
21
+ "userId": "user-mobile",
22
+ "cartValue": 50,
23
+ "deviceType": "mobile"
24
+ },
25
+ "expectedAssignments": {
26
+ "checkout.ctaText": "Buy Now",
27
+ "checkout.showUrgency": false
28
+ },
29
+ "comment": "Mobile user with low cart value gets mobile CTA"
30
+ },
31
+ {
32
+ "name": "desktop_user_low_cart",
33
+ "context": {
34
+ "userId": "user-desktop",
35
+ "cartValue": 50,
36
+ "deviceType": "desktop"
37
+ },
38
+ "expectedAssignments": {
39
+ "checkout.ctaText": "Complete Purchase",
40
+ "checkout.showUrgency": false
41
+ },
42
+ "comment": "Desktop user with low cart value gets defaults"
43
+ },
44
+ {
45
+ "name": "mobile_user_high_cart",
46
+ "context": {
47
+ "userId": "user-mobile-high",
48
+ "cartValue": 200,
49
+ "deviceType": "mobile"
50
+ },
51
+ "expectedAssignments": {
52
+ "checkout.ctaText": "Buy Now - Limited Stock!",
53
+ "checkout.showUrgency": true
54
+ },
55
+ "comment": "High value cart condition takes precedence (first matching policy)"
56
+ }
57
+ ]
58
+ }
59
+