@sonordev/contracts 2.18.0 → 2.20.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/CHANGELOG.md +34 -0
- package/README.md +454 -1
- package/dist/broadcast-layouts.d.ts +8 -0
- package/dist/broadcast-layouts.js +4 -0
- package/dist/broadcast-layouts.mjs +3 -1
- package/dist/funnels.d.ts +46 -0
- package/dist/funnels.js +35 -4
- package/dist/funnels.mjs +33 -5
- package/dist/resonance-constants.d.ts +4 -0
- package/dist/resonance-constants.js +10 -0
- package/dist/resonance-constants.mjs +6 -0
- package/dist/resonance.d.ts +6962 -0
- package/dist/resonance.js +1809 -0
- package/dist/resonance.mjs +1650 -0
- package/dist/signal-experiments.d.ts +219 -0
- package/dist/signal-experiments.js +270 -0
- package/dist/signal-experiments.mjs +260 -0
- package/dist/wire-schema.d.ts +36 -0
- package/docs.json +21 -0
- package/package.json +19 -3
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/** Existing Signal experiment management semantics. No assignment or inference engine. */
|
|
2
|
+
import { type Field, type Value } from './wire-schema';
|
|
3
|
+
export declare class SignalExperimentValidationError extends Error {
|
|
4
|
+
readonly path: string;
|
|
5
|
+
constructor(path: string, reason: string);
|
|
6
|
+
}
|
|
7
|
+
declare const variant: Field<{
|
|
8
|
+
key: string;
|
|
9
|
+
name: string;
|
|
10
|
+
weight: number;
|
|
11
|
+
} & {
|
|
12
|
+
description?: string | undefined;
|
|
13
|
+
}>;
|
|
14
|
+
declare const targeting: Field<{} & {
|
|
15
|
+
pages?: string[] | undefined;
|
|
16
|
+
segments?: string[] | undefined;
|
|
17
|
+
devices?: ("desktop" | "mobile" | "tablet")[] | undefined;
|
|
18
|
+
geo?: string[] | undefined;
|
|
19
|
+
}>;
|
|
20
|
+
declare const create: Field<{
|
|
21
|
+
name: string;
|
|
22
|
+
variants: ({
|
|
23
|
+
key: string;
|
|
24
|
+
name: string;
|
|
25
|
+
weight: number;
|
|
26
|
+
} & {
|
|
27
|
+
description?: string | undefined;
|
|
28
|
+
})[];
|
|
29
|
+
} & {
|
|
30
|
+
description?: string | undefined;
|
|
31
|
+
traffic_allocation?: number | undefined;
|
|
32
|
+
targeting_rules?: ({} & {
|
|
33
|
+
pages?: string[] | undefined;
|
|
34
|
+
segments?: string[] | undefined;
|
|
35
|
+
devices?: ("desktop" | "mobile" | "tablet")[] | undefined;
|
|
36
|
+
geo?: string[] | undefined;
|
|
37
|
+
}) | undefined;
|
|
38
|
+
}>;
|
|
39
|
+
declare const update: Field<{} & {
|
|
40
|
+
name?: string | undefined;
|
|
41
|
+
description?: string | undefined;
|
|
42
|
+
variants?: ({
|
|
43
|
+
key: string;
|
|
44
|
+
name: string;
|
|
45
|
+
weight: number;
|
|
46
|
+
} & {
|
|
47
|
+
description?: string | undefined;
|
|
48
|
+
})[] | undefined;
|
|
49
|
+
traffic_allocation?: number | undefined;
|
|
50
|
+
targeting_rules?: ({} & {
|
|
51
|
+
pages?: string[] | undefined;
|
|
52
|
+
segments?: string[] | undefined;
|
|
53
|
+
devices?: ("desktop" | "mobile" | "tablet")[] | undefined;
|
|
54
|
+
geo?: string[] | undefined;
|
|
55
|
+
}) | undefined;
|
|
56
|
+
}>;
|
|
57
|
+
export type SignalExperimentVariant = Value<typeof variant>;
|
|
58
|
+
export type SignalExperimentTargetingRules = Value<typeof targeting>;
|
|
59
|
+
export type SignalExperimentCreate = Value<typeof create>;
|
|
60
|
+
export type SignalExperimentUpdate = Value<typeof update>;
|
|
61
|
+
/** Leaves defaults to the existing service; parsing never grants Full-plan access. */
|
|
62
|
+
export declare const parseSignalExperimentCreate: (v: unknown) => SignalExperimentCreate;
|
|
63
|
+
export declare const parseSignalExperimentUpdate: (v: unknown) => SignalExperimentUpdate;
|
|
64
|
+
export declare const SIGNAL_EXPERIMENT_CREATE_JSON_SCHEMA: import("./wire-schema").JsonSchema;
|
|
65
|
+
export declare const SIGNAL_EXPERIMENT_UPDATE_JSON_SCHEMA: import("./wire-schema").JsonSchema;
|
|
66
|
+
declare const measurement: Field<({
|
|
67
|
+
schemaVersion: 1;
|
|
68
|
+
mode: "trusted_source";
|
|
69
|
+
state: "observed";
|
|
70
|
+
observedAt: string;
|
|
71
|
+
scope: {
|
|
72
|
+
projectId: string;
|
|
73
|
+
experimentId: string;
|
|
74
|
+
site: string;
|
|
75
|
+
experimentRevision: number;
|
|
76
|
+
goalRevision: string;
|
|
77
|
+
campaignId: string;
|
|
78
|
+
campaignVersionId: string;
|
|
79
|
+
assignedFrom: string;
|
|
80
|
+
assignedTo: string;
|
|
81
|
+
} & {};
|
|
82
|
+
countingUnit: "consent_subject";
|
|
83
|
+
coverage: "assigned_population_only";
|
|
84
|
+
semanticRole: "accepted_form_diagnostic";
|
|
85
|
+
businessMetrics: {
|
|
86
|
+
uniqueQualifiedContacts: {
|
|
87
|
+
state: "unavailable";
|
|
88
|
+
reasonCode: "original_qualified_lineage_not_bound";
|
|
89
|
+
} & {};
|
|
90
|
+
wonValue: {
|
|
91
|
+
state: "unavailable";
|
|
92
|
+
reasonCode: "original_deal_lineage_not_bound";
|
|
93
|
+
} & {};
|
|
94
|
+
paidRevenue: {
|
|
95
|
+
state: "unavailable";
|
|
96
|
+
reasonCode: "original_paid_source_lineage_not_bound";
|
|
97
|
+
} & {};
|
|
98
|
+
} & {};
|
|
99
|
+
originalGoal: {
|
|
100
|
+
sourceId: string;
|
|
101
|
+
sourceRevision: string;
|
|
102
|
+
countingUnit: "net_order" | "unique_qualified_contact" | "unique_source";
|
|
103
|
+
sourceKind: "accepted_form" | "qualified_lead";
|
|
104
|
+
} & {};
|
|
105
|
+
diagnosticType: "accepted_form";
|
|
106
|
+
diagnosticState: "available";
|
|
107
|
+
variants: ({
|
|
108
|
+
variantKey: string;
|
|
109
|
+
assignedUnits: string;
|
|
110
|
+
exposureEvents: string;
|
|
111
|
+
exposedUnits: string;
|
|
112
|
+
matureAssignedUnits: string;
|
|
113
|
+
immatureAssignedUnits: string;
|
|
114
|
+
assignmentsWithoutExposure: string;
|
|
115
|
+
acceptedFormEvents: string;
|
|
116
|
+
acceptedFormUnits: string;
|
|
117
|
+
matureAcceptedFormUnits: string;
|
|
118
|
+
} & {})[];
|
|
119
|
+
} & {}) | ({
|
|
120
|
+
schemaVersion: 1;
|
|
121
|
+
mode: "trusted_source";
|
|
122
|
+
state: "observed";
|
|
123
|
+
observedAt: string;
|
|
124
|
+
scope: {
|
|
125
|
+
projectId: string;
|
|
126
|
+
experimentId: string;
|
|
127
|
+
site: string;
|
|
128
|
+
experimentRevision: number;
|
|
129
|
+
goalRevision: string;
|
|
130
|
+
campaignId: string;
|
|
131
|
+
campaignVersionId: string;
|
|
132
|
+
assignedFrom: string;
|
|
133
|
+
assignedTo: string;
|
|
134
|
+
} & {};
|
|
135
|
+
countingUnit: "consent_subject";
|
|
136
|
+
coverage: "assigned_population_only";
|
|
137
|
+
semanticRole: "accepted_form_diagnostic";
|
|
138
|
+
businessMetrics: {
|
|
139
|
+
uniqueQualifiedContacts: {
|
|
140
|
+
state: "unavailable";
|
|
141
|
+
reasonCode: "original_qualified_lineage_not_bound";
|
|
142
|
+
} & {};
|
|
143
|
+
wonValue: {
|
|
144
|
+
state: "unavailable";
|
|
145
|
+
reasonCode: "original_deal_lineage_not_bound";
|
|
146
|
+
} & {};
|
|
147
|
+
paidRevenue: {
|
|
148
|
+
state: "unavailable";
|
|
149
|
+
reasonCode: "original_paid_source_lineage_not_bound";
|
|
150
|
+
} & {};
|
|
151
|
+
} & {};
|
|
152
|
+
originalGoal: {
|
|
153
|
+
sourceId: string;
|
|
154
|
+
sourceRevision: string;
|
|
155
|
+
countingUnit: "net_order" | "unique_qualified_contact" | "unique_source";
|
|
156
|
+
sourceKind: "approved_application" | "completed_booking" | "net_paid_order";
|
|
157
|
+
} & {};
|
|
158
|
+
diagnosticType: null;
|
|
159
|
+
diagnosticState: "unsupported_original_source";
|
|
160
|
+
variants: ({
|
|
161
|
+
variantKey: string;
|
|
162
|
+
assignedUnits: string;
|
|
163
|
+
exposureEvents: string;
|
|
164
|
+
exposedUnits: string;
|
|
165
|
+
matureAssignedUnits: string;
|
|
166
|
+
immatureAssignedUnits: string;
|
|
167
|
+
assignmentsWithoutExposure: string;
|
|
168
|
+
acceptedFormEvents: null;
|
|
169
|
+
acceptedFormUnits: null;
|
|
170
|
+
matureAcceptedFormUnits: null;
|
|
171
|
+
} & {})[];
|
|
172
|
+
} & {}) | ({
|
|
173
|
+
schemaVersion: 1;
|
|
174
|
+
mode: "legacy";
|
|
175
|
+
state: "unavailable";
|
|
176
|
+
reasonCode: "legacy_assignment_outcome_events_only";
|
|
177
|
+
} & {}) | ({
|
|
178
|
+
schemaVersion: 1;
|
|
179
|
+
mode: "trusted_source";
|
|
180
|
+
state: "unavailable";
|
|
181
|
+
reasonCode: "measurement_scope_required";
|
|
182
|
+
} & {})>;
|
|
183
|
+
export type SignalExperimentMeasurement = Value<typeof measurement>;
|
|
184
|
+
/** Decodes only the additive measurement block. Native scope, source facts and
|
|
185
|
+
* statement time must be authenticated by the existing results owner. Structural
|
|
186
|
+
* JSON Schema is shared; BigInt algebra and interval checks run in this parser. */
|
|
187
|
+
export declare const parseSignalExperimentMeasurement: (v: unknown) => SignalExperimentMeasurement;
|
|
188
|
+
export declare const SIGNAL_EXPERIMENT_MEASUREMENT_JSON_SCHEMA: import("./wire-schema").JsonSchema;
|
|
189
|
+
declare const scopeOptions: Field<{
|
|
190
|
+
observedAt: string;
|
|
191
|
+
items: ({
|
|
192
|
+
campaignId: string;
|
|
193
|
+
campaignVersionId: string;
|
|
194
|
+
campaignDefinitionRevision: number;
|
|
195
|
+
acceptedAt: string;
|
|
196
|
+
site: string;
|
|
197
|
+
experimentId: string;
|
|
198
|
+
experimentRevision: number;
|
|
199
|
+
goalRevision: string;
|
|
200
|
+
goalLabel: string | null;
|
|
201
|
+
assignmentExtent: ({
|
|
202
|
+
firstAssignedAt: string;
|
|
203
|
+
lastAssignedAt: string;
|
|
204
|
+
} & {}) | null;
|
|
205
|
+
suggestedCohort: ({
|
|
206
|
+
assignedFrom: string;
|
|
207
|
+
assignedTo: string;
|
|
208
|
+
} & {}) | null;
|
|
209
|
+
} & {})[];
|
|
210
|
+
nextCursor: string | null;
|
|
211
|
+
} & {}>;
|
|
212
|
+
export type SignalExperimentScopeOptions = Value<typeof scopeOptions>;
|
|
213
|
+
/** Structural decoding and internal time/tuple invariants only. Native scope,
|
|
214
|
+
* membership, acceptance and statement time remain the existing owner's facts.
|
|
215
|
+
* A shaped opaque cursor grants no read authority. JSON Schema documents the
|
|
216
|
+
* structural subset; cross-field ordering and tuple checks run in this parser. */
|
|
217
|
+
export declare const parseSignalExperimentScopeOptions: (v: unknown) => SignalExperimentScopeOptions;
|
|
218
|
+
export declare const SIGNAL_EXPERIMENT_SCOPE_OPTIONS_JSON_SCHEMA: import("./wire-schema").JsonSchema;
|
|
219
|
+
export {};
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/contracts/wire-schema.ts
|
|
4
|
+
function createWireSchema(fail2, isValidationError) {
|
|
5
|
+
const own = (v, k) => Object.prototype.hasOwnProperty.call(v, k);
|
|
6
|
+
const field2 = (schema, read) => ({ schema, read });
|
|
7
|
+
const text2 = (max = 200, pattern, min = 1) => field2({ type: "string", minLength: min, maxLength: max, ...pattern ? { pattern: pattern.source } : {} }, (v, p) => typeof v === "string" && v.length >= min && v.length <= max && (!pattern || pattern.test(v)) ? v : fail2(p, "invalid string"));
|
|
8
|
+
const choice2 = (...values) => field2({ type: "string", enum: values }, (v, p) => typeof v === "string" && values.includes(v) ? v : fail2(p, "unsupported value"));
|
|
9
|
+
const literal2 = (value) => field2({ const: value }, (v, p) => v === value ? value : fail2(p, "unexpected value"));
|
|
10
|
+
const integer2 = (min = 0, max = Number.MAX_SAFE_INTEGER) => field2({ type: "integer", minimum: min, maximum: max }, (v, p) => typeof v === "number" && Number.isSafeInteger(v) && v >= min && v <= max ? v : fail2(p, "invalid integer"));
|
|
11
|
+
const bool = field2({ type: "boolean" }, (v, p) => typeof v === "boolean" ? v : fail2(p, "must be boolean"));
|
|
12
|
+
const optional2 = (f) => ({ ...f, optional: true });
|
|
13
|
+
const nullable2 = (f) => field2({ anyOf: [f.schema, { type: "null" }] }, (v, p) => v === null ? null : f.read(v, p));
|
|
14
|
+
const list2 = (f, max = 100, min = 0) => field2({ type: "array", items: f.schema, minItems: min, maxItems: max }, (v, p) => {
|
|
15
|
+
if (!Array.isArray(v) || v.length < min || v.length > max) fail2(p, "invalid array");
|
|
16
|
+
return v.map((x, i) => f.read(x, `${p}[${i}]`));
|
|
17
|
+
});
|
|
18
|
+
const object2 = (fields2) => field2({ type: "object", additionalProperties: false, properties: Object.fromEntries(Object.entries(fields2).map(([k, f]) => [k, f.schema])), required: Object.keys(fields2).filter((k) => !("optional" in fields2[k])) }, (v, p) => {
|
|
19
|
+
if (!v || typeof v !== "object" || Array.isArray(v) || ![Object.prototype, null].includes(Object.getPrototypeOf(v))) fail2(p, "must be a plain object");
|
|
20
|
+
const source = v, result = {};
|
|
21
|
+
for (const key of Reflect.ownKeys(source)) if (typeof key !== "string" || !own(fields2, key)) fail2(p, "unknown field");
|
|
22
|
+
for (const [key, f] of Object.entries(fields2)) {
|
|
23
|
+
if (!own(source, key)) {
|
|
24
|
+
if ("optional" in f) continue;
|
|
25
|
+
fail2(`${p}.${key}`, "required");
|
|
26
|
+
}
|
|
27
|
+
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
|
28
|
+
if (!own(descriptor, "value")) fail2(`${p}.${key}`, "accessors are not wire data");
|
|
29
|
+
result[key] = f.read(descriptor.value, `${p}.${key}`);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
});
|
|
33
|
+
const union2 = (...fields2) => field2({ oneOf: fields2.map((f) => f.schema) }, (v, p) => {
|
|
34
|
+
const found = [];
|
|
35
|
+
for (const f of fields2) {
|
|
36
|
+
try {
|
|
37
|
+
found.push(f.read(v, p));
|
|
38
|
+
} catch (e) {
|
|
39
|
+
if (!isValidationError(e)) throw e;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return found.length === 1 ? found[0] : fail2(p, "must match exactly one supported shape");
|
|
43
|
+
});
|
|
44
|
+
const refine2 = (f, check) => field2(f.schema, (v, p) => {
|
|
45
|
+
const result = f.read(v, p);
|
|
46
|
+
check(result, p);
|
|
47
|
+
return result;
|
|
48
|
+
});
|
|
49
|
+
return { field: field2, text: text2, choice: choice2, literal: literal2, integer: integer2, bool, optional: optional2, nullable: nullable2, list: list2, object: object2, union: union2, refine: refine2 };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/contracts/sites-normalize.ts
|
|
53
|
+
function normalizeSiteHost(site) {
|
|
54
|
+
if (!site || typeof site !== "string") return null;
|
|
55
|
+
let s = site.trim().toLowerCase();
|
|
56
|
+
if (!s) return null;
|
|
57
|
+
s = s.replace(/^https?:\/\//, "");
|
|
58
|
+
const slash = s.indexOf("/");
|
|
59
|
+
if (slash >= 0) s = s.slice(0, slash);
|
|
60
|
+
const colon = s.indexOf(":");
|
|
61
|
+
if (colon >= 0) s = s.slice(0, colon);
|
|
62
|
+
s = s.replace(/\.+$/, "");
|
|
63
|
+
return s || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/contracts/sites.ts
|
|
67
|
+
function isNormalizedSiteHost(value) {
|
|
68
|
+
return typeof value === "string" && value.length > 0 && normalizeSiteHost(value) === value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/contracts/public-url-host.ts
|
|
72
|
+
function isPrivateIpv4(a, b) {
|
|
73
|
+
return a === 127 || a === 10 || a === 0 || a === 192 && b === 168 || a === 172 && b >= 16 && b <= 31 || a === 169 && b === 254 || a === 100 && b >= 64 && b <= 127 || a >= 224;
|
|
74
|
+
}
|
|
75
|
+
function ipv6Words(host) {
|
|
76
|
+
if (!host.includes(":") || !/^[a-f\d:.]+$/.test(host)) return null;
|
|
77
|
+
let canonical;
|
|
78
|
+
try {
|
|
79
|
+
canonical = new URL(`http://[${host}]/`).hostname.slice(1, -1);
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const sides = canonical.split("::");
|
|
84
|
+
const left = sides[0] ? sides[0].split(":") : [];
|
|
85
|
+
const right = sides.length > 1 && sides[1] ? sides[1].split(":") : [];
|
|
86
|
+
return [...left, ...Array(sides.length > 1 ? 8 - left.length - right.length : 0).fill("0"), ...right].map((word) => parseInt(word, 16));
|
|
87
|
+
}
|
|
88
|
+
function isBlockedPublicUrlHost(rawHost) {
|
|
89
|
+
const host = rawHost.toLowerCase().replace(/^\[|\]$/g, "");
|
|
90
|
+
const blockedName = host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal");
|
|
91
|
+
if (host.includes(":") && host.includes("%")) return true;
|
|
92
|
+
const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
93
|
+
const blockedIp = !!ipv4 && isPrivateIpv4(Number(ipv4[1]), Number(ipv4[2]));
|
|
94
|
+
const words = ipv6Words(host);
|
|
95
|
+
const blockedIpv6 = !!words && // Unspecified/loopback, unique-local, link-local, deprecated site-local
|
|
96
|
+
// and multicast destinations aren't public logo/webhook endpoints.
|
|
97
|
+
(words.every((word) => word === 0) || words.slice(0, 7).every((word) => word === 0) && words[7] === 1 || (words[0] & 65024) === 64512 || (words[0] & 65472) === 65152 || (words[0] & 65472) === 65216 || (words[0] & 65280) === 65280 || // IPv4-mapped and older IPv4-compatible forms carry an IPv4 destination.
|
|
98
|
+
words.slice(0, 5).every((word) => word === 0) && [0, 65535].includes(words[5]) && isPrivateIpv4(words[6] >> 8, words[6] & 255));
|
|
99
|
+
return blockedName || blockedIp || blockedIpv6;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/contracts/signal-experiments.ts
|
|
103
|
+
var SignalExperimentValidationError = class extends Error {
|
|
104
|
+
constructor(path, reason) {
|
|
105
|
+
super(`${path}: ${reason}`);
|
|
106
|
+
this.path = path;
|
|
107
|
+
this.name = "SignalExperimentValidationError";
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var fail = (path, reason) => {
|
|
111
|
+
throw new SignalExperimentValidationError(path, reason);
|
|
112
|
+
};
|
|
113
|
+
var { field, text, choice, literal, integer, optional, nullable, list, object, union, refine } = createWireSchema(fail, (error) => error instanceof SignalExperimentValidationError);
|
|
114
|
+
var percent = field({ type: "number", minimum: 0, maximum: 100 }, (v, p) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 100 ? v : fail(p, "must be a finite percentage from 0 to 100"));
|
|
115
|
+
var page = refine(text(2048, /^\/(?!\/)[^?#\s\\]*$/), (v, p) => {
|
|
116
|
+
const parsed = new URL(v, "https://example.com");
|
|
117
|
+
if (parsed.pathname !== v) fail(p, "must be a canonical page path");
|
|
118
|
+
});
|
|
119
|
+
var variant = object({ key: text(80), name: text(160), weight: percent, description: optional(text(1e3, void 0, 0)) });
|
|
120
|
+
var variants = refine(list(variant, 50, 2), (v, p) => {
|
|
121
|
+
if (new Set(v.map((x) => x.key)).size !== v.length) fail(p, "variant keys must be unique");
|
|
122
|
+
if (Math.abs(v.reduce((sum, x) => sum + x.weight, 0) - 100) > 0.01) fail(p, "variant weights must sum to 100");
|
|
123
|
+
});
|
|
124
|
+
var targeting = object({ pages: optional(list(page, 100)), segments: optional(list(text(160), 100)), devices: optional(list(choice("desktop", "mobile", "tablet"), 3)), geo: optional(list(text(160), 100)) });
|
|
125
|
+
var fields = { name: text(160), description: text(1e4, void 0, 0), variants, traffic_allocation: percent, targeting_rules: targeting };
|
|
126
|
+
var create = object({ ...fields, description: optional(fields.description), traffic_allocation: optional(fields.traffic_allocation), targeting_rules: optional(fields.targeting_rules) });
|
|
127
|
+
var update = object({ name: optional(fields.name), description: optional(fields.description), variants: optional(fields.variants), traffic_allocation: optional(fields.traffic_allocation), targeting_rules: optional(fields.targeting_rules) });
|
|
128
|
+
var parseSignalExperimentCreate = (v) => create.read(v, "experimentCreate");
|
|
129
|
+
var parseSignalExperimentUpdate = (v) => update.read(v, "experimentUpdate");
|
|
130
|
+
var SIGNAL_EXPERIMENT_CREATE_JSON_SCHEMA = create.schema;
|
|
131
|
+
var SIGNAL_EXPERIMENT_UPDATE_JSON_SCHEMA = update.schema;
|
|
132
|
+
var measurementUuid = text(36, /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
|
|
133
|
+
var measurementRevision = text(128, /^[a-zA-Z0-9._:-]+$/);
|
|
134
|
+
var measurementInstant = refine(text(30, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/), (v, p) => {
|
|
135
|
+
if (!Number.isFinite(Date.parse(v)) || new Date(v).toISOString().replace(".000Z", "Z") !== v.replace(".000Z", "Z")) fail(p, "invalid UTC instant");
|
|
136
|
+
});
|
|
137
|
+
var measurementSite = refine(text(253), (v, p) => {
|
|
138
|
+
if (!isNormalizedSiteHost(v) || isBlockedPublicUrlHost(v)) fail(p, "must be a normalized public site host");
|
|
139
|
+
});
|
|
140
|
+
var measurementCount = text(30, /^(?:0|[1-9][0-9]*)$/);
|
|
141
|
+
var measurementScope = object({
|
|
142
|
+
projectId: measurementUuid,
|
|
143
|
+
experimentId: measurementUuid,
|
|
144
|
+
site: measurementSite,
|
|
145
|
+
experimentRevision: integer(1),
|
|
146
|
+
goalRevision: measurementRevision,
|
|
147
|
+
campaignId: measurementUuid,
|
|
148
|
+
campaignVersionId: measurementUuid,
|
|
149
|
+
assignedFrom: measurementInstant,
|
|
150
|
+
assignedTo: measurementInstant
|
|
151
|
+
});
|
|
152
|
+
var assignmentCounts = {
|
|
153
|
+
variantKey: text(80),
|
|
154
|
+
assignedUnits: measurementCount,
|
|
155
|
+
exposureEvents: measurementCount,
|
|
156
|
+
exposedUnits: measurementCount,
|
|
157
|
+
matureAssignedUnits: measurementCount,
|
|
158
|
+
immatureAssignedUnits: measurementCount,
|
|
159
|
+
assignmentsWithoutExposure: measurementCount
|
|
160
|
+
};
|
|
161
|
+
var availableVariant = object({ ...assignmentCounts, acceptedFormEvents: measurementCount, acceptedFormUnits: measurementCount, matureAcceptedFormUnits: measurementCount });
|
|
162
|
+
var unsupportedVariant = object({ ...assignmentCounts, acceptedFormEvents: literal(null), acceptedFormUnits: literal(null), matureAcceptedFormUnits: literal(null) });
|
|
163
|
+
var businessMetrics = object({
|
|
164
|
+
uniqueQualifiedContacts: object({ state: literal("unavailable"), reasonCode: literal("original_qualified_lineage_not_bound") }),
|
|
165
|
+
wonValue: object({ state: literal("unavailable"), reasonCode: literal("original_deal_lineage_not_bound") }),
|
|
166
|
+
paidRevenue: object({ state: literal("unavailable"), reasonCode: literal("original_paid_source_lineage_not_bound") })
|
|
167
|
+
});
|
|
168
|
+
var goalCountingUnit = choice("unique_source", "unique_qualified_contact", "net_order");
|
|
169
|
+
var originalGoalFields = { sourceId: measurementUuid, sourceRevision: measurementRevision, countingUnit: goalCountingUnit };
|
|
170
|
+
var formSourceKind = choice("accepted_form", "qualified_lead");
|
|
171
|
+
var unsupportedSourceKind = choice("completed_booking", "net_paid_order", "approved_application");
|
|
172
|
+
var observedFields = {
|
|
173
|
+
schemaVersion: literal(1),
|
|
174
|
+
mode: literal("trusted_source"),
|
|
175
|
+
state: literal("observed"),
|
|
176
|
+
observedAt: measurementInstant,
|
|
177
|
+
scope: measurementScope,
|
|
178
|
+
countingUnit: literal("consent_subject"),
|
|
179
|
+
coverage: literal("assigned_population_only"),
|
|
180
|
+
semanticRole: literal("accepted_form_diagnostic"),
|
|
181
|
+
businessMetrics
|
|
182
|
+
};
|
|
183
|
+
var observedMeasurement = refine(union(
|
|
184
|
+
object({
|
|
185
|
+
...observedFields,
|
|
186
|
+
originalGoal: object({ ...originalGoalFields, sourceKind: formSourceKind }),
|
|
187
|
+
diagnosticType: literal("accepted_form"),
|
|
188
|
+
diagnosticState: literal("available"),
|
|
189
|
+
variants: list(availableVariant, 50)
|
|
190
|
+
}),
|
|
191
|
+
object({
|
|
192
|
+
...observedFields,
|
|
193
|
+
originalGoal: object({ ...originalGoalFields, sourceKind: unsupportedSourceKind }),
|
|
194
|
+
diagnosticType: literal(null),
|
|
195
|
+
diagnosticState: literal("unsupported_original_source"),
|
|
196
|
+
variants: list(unsupportedVariant, 50)
|
|
197
|
+
})
|
|
198
|
+
), (value, p) => {
|
|
199
|
+
if (Date.parse(value.scope.assignedFrom) >= Date.parse(value.scope.assignedTo) || Date.parse(value.scope.assignedTo) > Date.parse(value.observedAt))
|
|
200
|
+
fail(p + ".scope", "requires an increasing assignment cohort ending by the native observation");
|
|
201
|
+
if (new Set(value.variants.map((v) => v.variantKey)).size !== value.variants.length) fail(p + ".variants", "variant keys must be unique");
|
|
202
|
+
for (const [index, v] of value.variants.entries()) {
|
|
203
|
+
const assigned = BigInt(v.assignedUnits), exposed = BigInt(v.exposedUnits), mature = BigInt(v.matureAssignedUnits);
|
|
204
|
+
const at = `${p}.variants[${index}]`;
|
|
205
|
+
if (exposed > assigned || BigInt(v.exposureEvents) < exposed || BigInt(v.assignmentsWithoutExposure) !== assigned - exposed || mature + BigInt(v.immatureAssignedUnits) !== assigned)
|
|
206
|
+
fail(at, "assignment, exposure and maturity counts are inconsistent");
|
|
207
|
+
if (v.acceptedFormUnits !== null && v.acceptedFormEvents !== null && v.matureAcceptedFormUnits !== null) {
|
|
208
|
+
const accepted = BigInt(v.acceptedFormUnits), matureAccepted = BigInt(v.matureAcceptedFormUnits);
|
|
209
|
+
if (accepted > exposed || accepted > BigInt(v.acceptedFormEvents) || matureAccepted > mature || matureAccepted > accepted)
|
|
210
|
+
fail(at, "accepted form units must remain within their exposed, event and mature cohorts");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
var measurement = union(
|
|
215
|
+
object({ schemaVersion: literal(1), mode: literal("legacy"), state: literal("unavailable"), reasonCode: literal("legacy_assignment_outcome_events_only") }),
|
|
216
|
+
object({ schemaVersion: literal(1), mode: literal("trusted_source"), state: literal("unavailable"), reasonCode: literal("measurement_scope_required") }),
|
|
217
|
+
observedMeasurement
|
|
218
|
+
);
|
|
219
|
+
var parseSignalExperimentMeasurement = (v) => measurement.read(v, "experimentMeasurement");
|
|
220
|
+
var SIGNAL_EXPERIMENT_MEASUREMENT_JSON_SCHEMA = measurement.schema;
|
|
221
|
+
var scopeOption = object({
|
|
222
|
+
campaignId: measurementUuid,
|
|
223
|
+
campaignVersionId: measurementUuid,
|
|
224
|
+
campaignDefinitionRevision: integer(1),
|
|
225
|
+
acceptedAt: measurementInstant,
|
|
226
|
+
site: measurementSite,
|
|
227
|
+
experimentId: measurementUuid,
|
|
228
|
+
experimentRevision: integer(1),
|
|
229
|
+
goalRevision: measurementRevision,
|
|
230
|
+
goalLabel: nullable(text(200)),
|
|
231
|
+
assignmentExtent: nullable(object({ firstAssignedAt: measurementInstant, lastAssignedAt: measurementInstant })),
|
|
232
|
+
suggestedCohort: nullable(object({ assignedFrom: measurementInstant, assignedTo: measurementInstant }))
|
|
233
|
+
});
|
|
234
|
+
var scopeOptions = refine(object({ observedAt: measurementInstant, items: list(scopeOption, 100), nextCursor: nullable(text(4096)) }), (value, p) => {
|
|
235
|
+
const observed = Date.parse(value.observedAt), tuples = /* @__PURE__ */ new Set();
|
|
236
|
+
for (const [index, item] of value.items.entries()) {
|
|
237
|
+
const at = `${p}.items[${index}]`, accepted = Date.parse(item.acceptedAt);
|
|
238
|
+
const tuple = JSON.stringify([
|
|
239
|
+
item.campaignId.toLowerCase(),
|
|
240
|
+
item.campaignVersionId.toLowerCase(),
|
|
241
|
+
item.site,
|
|
242
|
+
item.experimentId.toLowerCase(),
|
|
243
|
+
item.experimentRevision,
|
|
244
|
+
item.goalRevision
|
|
245
|
+
]);
|
|
246
|
+
if (tuples.has(tuple)) fail(at, "original scope tuples must be unique");
|
|
247
|
+
tuples.add(tuple);
|
|
248
|
+
if (accepted > observed) fail(at + ".acceptedAt", "must not follow the native observation");
|
|
249
|
+
if (item.assignmentExtent) {
|
|
250
|
+
const first = Date.parse(item.assignmentExtent.firstAssignedAt), last = Date.parse(item.assignmentExtent.lastAssignedAt);
|
|
251
|
+
if (first > last || last > observed) fail(at + ".assignmentExtent", "requires first <= last <= native observation");
|
|
252
|
+
}
|
|
253
|
+
if (item.suggestedCohort) {
|
|
254
|
+
const from = Date.parse(item.suggestedCohort.assignedFrom), to = Date.parse(item.suggestedCohort.assignedTo);
|
|
255
|
+
if (from !== accepted || from >= to || to > observed) fail(at + ".suggestedCohort", "requires original acceptance start and increasing cohort ending by native observation");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
var parseSignalExperimentScopeOptions = (v) => scopeOptions.read(v, "experimentScopeOptions");
|
|
260
|
+
var SIGNAL_EXPERIMENT_SCOPE_OPTIONS_JSON_SCHEMA = scopeOptions.schema;
|
|
261
|
+
|
|
262
|
+
exports.SIGNAL_EXPERIMENT_CREATE_JSON_SCHEMA = SIGNAL_EXPERIMENT_CREATE_JSON_SCHEMA;
|
|
263
|
+
exports.SIGNAL_EXPERIMENT_MEASUREMENT_JSON_SCHEMA = SIGNAL_EXPERIMENT_MEASUREMENT_JSON_SCHEMA;
|
|
264
|
+
exports.SIGNAL_EXPERIMENT_SCOPE_OPTIONS_JSON_SCHEMA = SIGNAL_EXPERIMENT_SCOPE_OPTIONS_JSON_SCHEMA;
|
|
265
|
+
exports.SIGNAL_EXPERIMENT_UPDATE_JSON_SCHEMA = SIGNAL_EXPERIMENT_UPDATE_JSON_SCHEMA;
|
|
266
|
+
exports.SignalExperimentValidationError = SignalExperimentValidationError;
|
|
267
|
+
exports.parseSignalExperimentCreate = parseSignalExperimentCreate;
|
|
268
|
+
exports.parseSignalExperimentMeasurement = parseSignalExperimentMeasurement;
|
|
269
|
+
exports.parseSignalExperimentScopeOptions = parseSignalExperimentScopeOptions;
|
|
270
|
+
exports.parseSignalExperimentUpdate = parseSignalExperimentUpdate;
|