@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 +22 -0
- package/README.md +108 -0
- package/index.cjs +37 -0
- package/index.d.ts +81 -0
- package/index.js +38 -0
- package/package.json +43 -0
- package/schemas/config-bundle.schema.json +208 -0
- package/schemas/events.schema.json +239 -0
- package/schemas/traffical-config.schema.json +187 -0
- package/test-vectors/README.md +77 -0
- package/test-vectors/fixtures/bundle_basic.json +89 -0
- package/test-vectors/fixtures/bundle_conditions.json +76 -0
- package/test-vectors/fixtures/expected_basic.json +72 -0
- package/test-vectors/fixtures/expected_conditions.json +59 -0
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
|
+
|