@reopt-ai/data-contract 0.3.0 → 0.6.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/README.md +11 -4
- package/dist/{chunk-2LJ7JLDX.js → chunk-33BCA2LH.js} +8 -16
- package/dist/chunk-33BCA2LH.js.map +1 -0
- package/dist/chunk-3EEANNYN.cjs +62 -0
- package/dist/chunk-3EEANNYN.cjs.map +1 -0
- package/dist/{chunk-3A7WZ7L6.js → chunk-5BRCHDS3.js} +26 -7
- package/dist/chunk-5BRCHDS3.js.map +1 -0
- package/dist/chunk-6ZJOC4NJ.js +111 -0
- package/dist/chunk-6ZJOC4NJ.js.map +1 -0
- package/dist/chunk-FP2X3PRK.js +62 -0
- package/dist/chunk-FP2X3PRK.js.map +1 -0
- package/dist/{chunk-I2T4HRBM.cjs → chunk-FUGY5ANJ.cjs} +131 -19
- package/dist/chunk-FUGY5ANJ.cjs.map +1 -0
- package/dist/chunk-IXPP5K4I.js +19 -0
- package/dist/chunk-IXPP5K4I.js.map +1 -0
- package/dist/{chunk-J4VD3TLV.js → chunk-IY4MXH35.js} +30 -4
- package/dist/chunk-IY4MXH35.js.map +1 -0
- package/dist/chunk-JHHCT3GA.cjs +19 -0
- package/dist/chunk-JHHCT3GA.cjs.map +1 -0
- package/dist/chunk-KRJPEMFD.cjs +111 -0
- package/dist/chunk-KRJPEMFD.cjs.map +1 -0
- package/dist/{chunk-55IWO4S3.js → chunk-L3WJGIKD.js} +123 -11
- package/dist/chunk-L3WJGIKD.js.map +1 -0
- package/dist/{chunk-ATPS4UV6.cjs → chunk-LA5F5KR6.cjs} +33 -7
- package/dist/chunk-LA5F5KR6.cjs.map +1 -0
- package/dist/{chunk-RDPPPFGO.cjs → chunk-OGL3P7IH.cjs} +30 -11
- package/dist/chunk-OGL3P7IH.cjs.map +1 -0
- package/dist/{chunk-KXEQGD3E.cjs → chunk-YSAEOYPD.cjs} +9 -17
- package/dist/chunk-YSAEOYPD.cjs.map +1 -0
- package/dist/client.cjs +87 -43
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +8 -2
- package/dist/client.d.ts +8 -2
- package/dist/client.js +50 -6
- package/dist/client.js.map +1 -1
- package/dist/control.cjs +15 -4
- package/dist/control.cjs.map +1 -1
- package/dist/control.d.cts +43 -1
- package/dist/control.d.ts +43 -1
- package/dist/control.js +16 -5
- package/dist/events.cjs +17 -0
- package/dist/events.cjs.map +1 -0
- package/dist/events.d.cts +59 -0
- package/dist/events.d.ts +59 -0
- package/dist/events.js +17 -0
- package/dist/events.js.map +1 -0
- package/dist/identity.cjs +47 -0
- package/dist/identity.cjs.map +1 -0
- package/dist/identity.d.cts +90 -0
- package/dist/identity.d.ts +90 -0
- package/dist/identity.js +47 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -17
- package/dist/index.d.ts +7 -17
- package/dist/index.js +7 -5
- package/dist/ingest.cjs +16 -2
- package/dist/ingest.cjs.map +1 -1
- package/dist/ingest.d.cts +6 -3
- package/dist/ingest.d.ts +6 -3
- package/dist/ingest.js +16 -2
- package/dist/query.cjs +28 -3
- package/dist/query.cjs.map +1 -1
- package/dist/query.d.cts +299 -18
- package/dist/query.d.ts +299 -18
- package/dist/query.js +29 -4
- package/dist/report.cjs +85 -0
- package/dist/report.cjs.map +1 -0
- package/dist/report.d.cts +167 -0
- package/dist/report.d.ts +167 -0
- package/dist/report.js +85 -0
- package/dist/report.js.map +1 -0
- package/dist/version-CC4_BBvq.d.cts +26 -0
- package/dist/version-CC4_BBvq.d.ts +26 -0
- package/package.json +18 -2
- package/dist/chunk-2LJ7JLDX.js.map +0 -1
- package/dist/chunk-3A7WZ7L6.js.map +0 -1
- package/dist/chunk-55IWO4S3.js.map +0 -1
- package/dist/chunk-ATPS4UV6.cjs.map +0 -1
- package/dist/chunk-I2T4HRBM.cjs.map +0 -1
- package/dist/chunk-J4VD3TLV.js.map +0 -1
- package/dist/chunk-KXEQGD3E.cjs.map +0 -1
- package/dist/chunk-RDPPPFGO.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -71,18 +71,25 @@ const { data, meta } = await client.query.eventsTimeseries({
|
|
|
71
71
|
endDate: "2026-08-23",
|
|
72
72
|
granularity: "day",
|
|
73
73
|
eventName: "checkout_completed",
|
|
74
|
+
// Top-level event properties. Multiple filters are combined with AND.
|
|
75
|
+
propertyFilters: [{ key: "surface", operator: "eq", value: "brandfront" }],
|
|
76
|
+
breakdown: { kind: "property", key: "page_id", topN: 100 },
|
|
74
77
|
});
|
|
75
78
|
```
|
|
76
79
|
|
|
80
|
+
`propertyFilters` currently supports exact string equality only. Breakdown
|
|
81
|
+
values are capped at 100 so page-scale tables stay bounded.
|
|
82
|
+
|
|
77
83
|
`meta` reports staleness as two separate numbers, never one sum, because they
|
|
78
84
|
have different remedies:
|
|
79
85
|
|
|
80
86
|
- `cacheAgeSeconds` — how old the returned computation is. Resolves itself at
|
|
81
87
|
the next TTL.
|
|
82
|
-
- `ingestLagSeconds` — how far behind materialization was
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
- `ingestLagSeconds` — how far behind live materialization was. `0` means no
|
|
89
|
+
live event is pending; `null` plus `ingestLagStatus: "unknown"` means the
|
|
90
|
+
measurement failed and must not be shown as fresh.
|
|
91
|
+
- `ingestDeadLetterCount` — pending events older than the live replay window.
|
|
92
|
+
These remain visible without pegging the live "aggregating" indicator.
|
|
86
93
|
|
|
87
94
|
Not `now - max(events.created_at)`: that conflates a lagging pipeline with a
|
|
88
95
|
quiet project and grows without bound while a project sits idle.
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
var
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
var zDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "expected YYYY-MM-DD (UTC)");
|
|
3
|
+
var zDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "expected YYYY-MM-DD").refine(isRealCalendarDate, "date does not exist");
|
|
4
|
+
function isRealCalendarDate(value) {
|
|
5
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
6
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
7
|
+
const instant = new Date(Date.UTC(year, month - 1, day));
|
|
8
|
+
return instant.getUTCFullYear() === year && instant.getUTCMonth() === month - 1 && instant.getUTCDate() === day;
|
|
9
|
+
}
|
|
11
10
|
var zInstant = z.string().min(20);
|
|
12
11
|
var DataApiError = class _DataApiError extends Error {
|
|
13
12
|
/**
|
|
@@ -47,15 +46,8 @@ var DataApiError = class _DataApiError extends Error {
|
|
|
47
46
|
};
|
|
48
47
|
|
|
49
48
|
export {
|
|
50
|
-
CONTRACT_VERSION,
|
|
51
|
-
CONTRACT_VERSION_HEADER,
|
|
52
|
-
CLIENT_ID_HEADER,
|
|
53
|
-
CLIENT_SECRET_HEADER,
|
|
54
|
-
WRITE_KEY_HEADER,
|
|
55
|
-
DEVICE_ID_HEADER,
|
|
56
|
-
REQUEST_ID_HEADER,
|
|
57
49
|
zDate,
|
|
58
50
|
zInstant,
|
|
59
51
|
DataApiError
|
|
60
52
|
};
|
|
61
|
-
//# sourceMappingURL=chunk-
|
|
53
|
+
//# sourceMappingURL=chunk-33BCA2LH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@reopt-ai/data-contract` — the wire contract between reopt-data and its\n * server-credential consumers (RFC-0023).\n *\n * Subpaths:\n * `@reopt-ai/data-contract` this module — version, shared primitives, errors\n * `@reopt-ai/data-contract/ingest` POST /api/track request + response schemas\n * `@reopt-ai/data-contract/query` POST /api/v1/query/* request + response schemas\n * `@reopt-ai/data-contract/control` provisioning request + response schemas\n * `@reopt-ai/data-contract/client` createDataClient() — typed fetch over all\n * `@reopt-ai/data-contract/events` event names + property keys (no zod)\n * `@reopt-ai/data-contract/identity` cookie/header names and encodings shared by\n * the SDKs, the Next.js proxy and ingest\n *\n * Only `/client` touches `fetch`; everything else is pure zod. The single\n * runtime dependency is zod, deliberately — consumers bundle this.\n */\nimport { z } from \"zod\";\n\nexport {\n CONTRACT_VERSION,\n CONTRACT_VERSION_HEADER,\n CLIENT_ID_HEADER,\n CLIENT_SECRET_HEADER,\n WRITE_KEY_HEADER,\n DEVICE_ID_HEADER,\n REQUEST_ID_HEADER,\n} from \"./version.js\";\n\n/**\n * A real calendar date. The query's resolved IANA zone decides which instants\n * its inclusive day covers.\n */\nexport const zDate = z\n .string()\n .regex(/^\\d{4}-\\d{2}-\\d{2}$/, \"expected YYYY-MM-DD\")\n .refine(isRealCalendarDate, \"date does not exist\");\n\nfunction isRealCalendarDate(value: string): boolean {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return false;\n const [year, month, day] = value.split(\"-\").map(Number);\n const instant = new Date(Date.UTC(year!, month! - 1, day!));\n return instant.getUTCFullYear() === year && instant.getUTCMonth() === month! - 1 && instant.getUTCDate() === day;\n}\n\n/** ISO-8601 instant, as produced by `Date#toISOString()`. */\nexport const zInstant = z.string().min(20);\n\n/**\n * Client credentials. The same pair authenticates both planes; what a given\n * client may reach is decided server-side by `Client.scopes`\n * (`\"ingest\"` / `\"query\"` / `\"provision\"`).\n */\nexport interface ClientCredentials {\n clientId: string;\n clientSecret: string;\n}\n\n/**\n * Credentials are optional on the client itself because the provisioning\n * surface does not use them — it authenticates per call with a platform or\n * organization key. A caller that only provisions should not have to invent an\n * empty client id to satisfy a constructor.\n *\n * Reaching ingest or query without them fails with `credentials_missing`\n * rather than sending an unauthenticated request and reporting the 401 as a\n * server problem.\n */\nexport type OptionalClientCredentials = Partial<ClientCredentials>;\n\n/**\n * Every non-2xx response from either plane, plus the two failures the client\n * itself raises (`contract_mismatch`, `network_error`).\n */\nexport class DataApiError extends Error {\n /**\n * Brand for {@link DataApiError.is}. A bundler that inlines this module into\n * more than one entry point produces two distinct classes, and `instanceof`\n * silently stops matching — the brand survives that, and realm boundaries.\n */\n readonly isDataApiError = true as const;\n\n readonly status: number;\n readonly code: string;\n readonly requestId?: string;\n /** Parsed from `Retry-After`, in milliseconds. Only set on 429. */\n readonly retryAfterMs?: number;\n /** zod issues for `validation_failed`, or the raw body when unparseable. */\n readonly details?: unknown;\n\n constructor(init: {\n message: string;\n status: number;\n code: string;\n requestId?: string;\n retryAfterMs?: number;\n details?: unknown;\n }) {\n super(init.message);\n this.name = \"DataApiError\";\n this.status = init.status;\n this.code = init.code;\n this.requestId = init.requestId;\n this.retryAfterMs = init.retryAfterMs;\n this.details = init.details;\n }\n\n /** 429 and 5xx are worth retrying; 4xx are the caller's fault and are not. */\n get retryable(): boolean {\n return this.status === 429 || this.status >= 500;\n }\n\n /**\n * Prefer this over `instanceof` in a `catch`. It holds even when the class\n * has been duplicated — across CJS entry points, across bundler chunks, or\n * across realms — where `instanceof` quietly returns false.\n */\n static is(value: unknown): value is DataApiError {\n return (\n value instanceof DataApiError ||\n (typeof value === \"object\" && value !== null && (value as { isDataApiError?: unknown }).isDataApiError === true)\n );\n }\n}\n"],"mappings":";AAiBA,SAAS,SAAS;AAgBX,IAAM,QAAQ,EAClB,OAAO,EACP,MAAM,uBAAuB,qBAAqB,EAClD,OAAO,oBAAoB,qBAAqB;AAEnD,SAAS,mBAAmB,OAAwB;AAClD,MAAI,CAAC,sBAAsB,KAAK,KAAK,EAAG,QAAO;AAC/C,QAAM,CAAC,MAAM,OAAO,GAAG,IAAI,MAAM,MAAM,GAAG,EAAE,IAAI,MAAM;AACtD,QAAM,UAAU,IAAI,KAAK,KAAK,IAAI,MAAO,QAAS,GAAG,GAAI,CAAC;AAC1D,SAAO,QAAQ,eAAe,MAAM,QAAQ,QAAQ,YAAY,MAAM,QAAS,KAAK,QAAQ,WAAW,MAAM;AAC/G;AAGO,IAAM,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE;AA4BlC,IAAM,eAAN,MAAM,sBAAqB,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM7B,iBAAiB;AAAA,EAEjB;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EAET,YAAY,MAOT;AACD,UAAM,KAAK,OAAO;AAClB,SAAK,OAAO;AACZ,SAAK,SAAS,KAAK;AACnB,SAAK,OAAO,KAAK;AACjB,SAAK,YAAY,KAAK;AACtB,SAAK,eAAe,KAAK;AACzB,SAAK,UAAU,KAAK;AAAA,EACtB;AAAA;AAAA,EAGA,IAAI,YAAqB;AACvB,WAAO,KAAK,WAAW,OAAO,KAAK,UAAU;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAO,GAAG,OAAuC;AAC/C,WACE,iBAAiB,iBAChB,OAAO,UAAU,YAAY,UAAU,QAAS,MAAuC,mBAAmB;AAAA,EAE/G;AACF;","names":[]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/events.ts
|
|
2
|
+
var RESERVED_EVENT_NAMES = ["session_start", "session_end", "screen_view"];
|
|
3
|
+
var AUTO_EVENT_NAMES = {
|
|
4
|
+
pageview: "$pageview",
|
|
5
|
+
screenView: "$screen_view",
|
|
6
|
+
pageleave: "$pageleave",
|
|
7
|
+
webVitals: "$web_vitals",
|
|
8
|
+
exception: "$exception"
|
|
9
|
+
};
|
|
10
|
+
var AUTO_EVENT_PROPERTIES = {
|
|
11
|
+
[AUTO_EVENT_NAMES.pageview]: [
|
|
12
|
+
"path",
|
|
13
|
+
"origin",
|
|
14
|
+
"title",
|
|
15
|
+
"referrer",
|
|
16
|
+
"search",
|
|
17
|
+
"utm_source",
|
|
18
|
+
"utm_medium",
|
|
19
|
+
"utm_campaign",
|
|
20
|
+
"utm_term",
|
|
21
|
+
"utm_content"
|
|
22
|
+
],
|
|
23
|
+
[AUTO_EVENT_NAMES.screenView]: ["screen_name"],
|
|
24
|
+
[AUTO_EVENT_NAMES.pageleave]: ["path", "origin", "duration", "scroll_depth", "scroll_depth_bucket"],
|
|
25
|
+
[AUTO_EVENT_NAMES.webVitals]: ["metric_name", "metric_id", "value", "delta", "rating", "navigation_type", "path"],
|
|
26
|
+
// `$`-prefixed on purpose: these are reserved, SDK-owned keys with fixed
|
|
27
|
+
// semantics, distinguished from a host's own snake_case vocabulary the
|
|
28
|
+
// same way the event names are. Not a typo — do not strip the prefix.
|
|
29
|
+
[AUTO_EVENT_NAMES.exception]: [
|
|
30
|
+
"$exception_type",
|
|
31
|
+
"$exception_message",
|
|
32
|
+
"$exception_stack",
|
|
33
|
+
"$exception_source",
|
|
34
|
+
"$exception_handled",
|
|
35
|
+
"path"
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
var AUTO_EVENT_ROLLUP_KEYS = {
|
|
39
|
+
[AUTO_EVENT_NAMES.pageview]: ["path", "origin", "referrer", "utm_source", "utm_medium", "utm_campaign"],
|
|
40
|
+
[AUTO_EVENT_NAMES.screenView]: ["screen_name"],
|
|
41
|
+
[AUTO_EVENT_NAMES.pageleave]: ["path", "origin", "scroll_depth_bucket"],
|
|
42
|
+
[AUTO_EVENT_NAMES.webVitals]: ["metric_name", "rating", "navigation_type", "path"],
|
|
43
|
+
[AUTO_EVENT_NAMES.exception]: ["$exception_type", "$exception_source", "$exception_handled", "path"]
|
|
44
|
+
};
|
|
45
|
+
var SCROLL_DEPTH_BUCKETS = [25, 50, 75, 100];
|
|
46
|
+
function scrollDepthBucket(depth) {
|
|
47
|
+
let reached = "0";
|
|
48
|
+
for (const threshold of SCROLL_DEPTH_BUCKETS) {
|
|
49
|
+
if (depth >= threshold) reached = `${threshold}`;
|
|
50
|
+
}
|
|
51
|
+
return reached;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
exports.RESERVED_EVENT_NAMES = RESERVED_EVENT_NAMES; exports.AUTO_EVENT_NAMES = AUTO_EVENT_NAMES; exports.AUTO_EVENT_PROPERTIES = AUTO_EVENT_PROPERTIES; exports.AUTO_EVENT_ROLLUP_KEYS = AUTO_EVENT_ROLLUP_KEYS; exports.SCROLL_DEPTH_BUCKETS = SCROLL_DEPTH_BUCKETS; exports.scrollDepthBucket = scrollDepthBucket;
|
|
62
|
+
//# sourceMappingURL=chunk-3EEANNYN.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-3EEANNYN.cjs","../src/events.ts"],"names":[],"mappings":"AAAA;ACOO,IAAM,qBAAA,EAAuB,CAAC,eAAA,EAAiB,aAAA,EAAe,aAAa,CAAA;AAQ3E,IAAM,iBAAA,EAAmB;AAAA,EAC9B,QAAA,EAAU,WAAA;AAAA,EACV,UAAA,EAAY,cAAA;AAAA,EACZ,SAAA,EAAW,YAAA;AAAA,EACX,SAAA,EAAW,aAAA;AAAA,EACX,SAAA,EAAW;AACb,CAAA;AASO,IAAM,sBAAA,EAAwB;AAAA,EACnC,CAAC,gBAAA,CAAiB,QAAQ,CAAA,EAAG;AAAA,IAC3B,MAAA;AAAA,IACA,QAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAA;AAAA,IACA,QAAA;AAAA,IACA,YAAA;AAAA,IACA,YAAA;AAAA,IACA,cAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,EACF,CAAA;AAAA,EACA,CAAC,gBAAA,CAAiB,UAAU,CAAA,EAAG,CAAC,aAAa,CAAA;AAAA,EAC7C,CAAC,gBAAA,CAAiB,SAAS,CAAA,EAAG,CAAC,MAAA,EAAQ,QAAA,EAAU,UAAA,EAAY,cAAA,EAAgB,qBAAqB,CAAA;AAAA,EAClG,CAAC,gBAAA,CAAiB,SAAS,CAAA,EAAG,CAAC,aAAA,EAAe,WAAA,EAAa,OAAA,EAAS,OAAA,EAAS,QAAA,EAAU,iBAAA,EAAmB,MAAM,CAAA;AAAA;AAAA;AAAA;AAAA,EAIhH,CAAC,gBAAA,CAAiB,SAAS,CAAA,EAAG;AAAA,IAC5B,iBAAA;AAAA,IACA,oBAAA;AAAA,IACA,kBAAA;AAAA,IACA,mBAAA;AAAA,IACA,oBAAA;AAAA,IACA;AAAA,EACF;AACF,CAAA;AAUO,IAAM,uBAAA,EAAyB;AAAA,EACpC,CAAC,gBAAA,CAAiB,QAAQ,CAAA,EAAG,CAAC,MAAA,EAAQ,QAAA,EAAU,UAAA,EAAY,YAAA,EAAc,YAAA,EAAc,cAAc,CAAA;AAAA,EACtG,CAAC,gBAAA,CAAiB,UAAU,CAAA,EAAG,CAAC,aAAa,CAAA;AAAA,EAC7C,CAAC,gBAAA,CAAiB,SAAS,CAAA,EAAG,CAAC,MAAA,EAAQ,QAAA,EAAU,qBAAqB,CAAA;AAAA,EACtE,CAAC,gBAAA,CAAiB,SAAS,CAAA,EAAG,CAAC,aAAA,EAAe,QAAA,EAAU,iBAAA,EAAmB,MAAM,CAAA;AAAA,EACjF,CAAC,gBAAA,CAAiB,SAAS,CAAA,EAAG,CAAC,iBAAA,EAAmB,mBAAA,EAAqB,oBAAA,EAAsB,MAAM;AACrG,CAAA;AAOO,IAAM,qBAAA,EAAuB,CAAC,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,GAAG,CAAA;AAG7C,SAAS,iBAAA,CAAkB,KAAA,EAAkC;AAClE,EAAA,IAAI,QAAA,EAA6B,GAAA;AACjC,EAAA,IAAA,CAAA,MAAW,UAAA,GAAa,oBAAA,EAAsB;AAC5C,IAAA,GAAA,CAAI,MAAA,GAAS,SAAA,EAAW,QAAA,EAAU,CAAA,EAAA;AACpC,EAAA;AACO,EAAA;AACT;ADrCuC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-3EEANNYN.cjs","sourcesContent":[null,"/**\n * `@reopt-ai/data-contract/events` — event names and property keys, with no\n * zod. Browser SDKs import this so their production bundles stay free of\n * schema code; `./ingest` re-exports everything here.\n */\n\n/** Event names the pipeline reserves for its own session bookkeeping. */\nexport const RESERVED_EVENT_NAMES = [\"session_start\", \"session_end\", \"screen_view\"] as const;\n\n/**\n * Events the SDKs emit on their own. One declaration, so a consumer that\n * registers them in its event catalogue (to make them roll up) cannot drift\n * from what the SDK actually sends — a drifted name silently falls back to\n * raw queries, which is slower but not visibly wrong.\n */\nexport const AUTO_EVENT_NAMES = {\n pageview: \"$pageview\",\n screenView: \"$screen_view\",\n pageleave: \"$pageleave\",\n webVitals: \"$web_vitals\",\n exception: \"$exception\",\n} as const;\nexport type AutoEventName = (typeof AUTO_EVENT_NAMES)[keyof typeof AUTO_EVENT_NAMES];\n\n/**\n * Property keys each automatic event carries. Consumers derive their\n * breakdown / rollup property lists from here rather than retyping them.\n * `path`, `origin`, `referrer` and `utm_*` are the keys the ingest pipeline\n * reads into session and event columns.\n */\nexport const AUTO_EVENT_PROPERTIES = {\n [AUTO_EVENT_NAMES.pageview]: [\n \"path\",\n \"origin\",\n \"title\",\n \"referrer\",\n \"search\",\n \"utm_source\",\n \"utm_medium\",\n \"utm_campaign\",\n \"utm_term\",\n \"utm_content\",\n ],\n [AUTO_EVENT_NAMES.screenView]: [\"screen_name\"],\n [AUTO_EVENT_NAMES.pageleave]: [\"path\", \"origin\", \"duration\", \"scroll_depth\", \"scroll_depth_bucket\"],\n [AUTO_EVENT_NAMES.webVitals]: [\"metric_name\", \"metric_id\", \"value\", \"delta\", \"rating\", \"navigation_type\", \"path\"],\n // `$`-prefixed on purpose: these are reserved, SDK-owned keys with fixed\n // semantics, distinguished from a host's own snake_case vocabulary the\n // same way the event names are. Not a typo — do not strip the prefix.\n [AUTO_EVENT_NAMES.exception]: [\n \"$exception_type\",\n \"$exception_message\",\n \"$exception_stack\",\n \"$exception_source\",\n \"$exception_handled\",\n \"path\",\n ],\n} as const satisfies Record<AutoEventName, readonly string[]>;\n\n/**\n * The subset of {@link AUTO_EVENT_PROPERTIES} that is a sensible rollup\n * dimension: low cardinality, categorical. Per-measurement ids and\n * continuous values (`metric_id`, `value`, `delta`, `duration`,\n * `scroll_depth`) are deliberately absent — as dimensions they would make\n * the rollup larger than the raw table it summarises. Derive\n * `rollupProperties` from here, not from the full property list.\n */\nexport const AUTO_EVENT_ROLLUP_KEYS = {\n [AUTO_EVENT_NAMES.pageview]: [\"path\", \"origin\", \"referrer\", \"utm_source\", \"utm_medium\", \"utm_campaign\"],\n [AUTO_EVENT_NAMES.screenView]: [\"screen_name\"],\n [AUTO_EVENT_NAMES.pageleave]: [\"path\", \"origin\", \"scroll_depth_bucket\"],\n [AUTO_EVENT_NAMES.webVitals]: [\"metric_name\", \"rating\", \"navigation_type\", \"path\"],\n [AUTO_EVENT_NAMES.exception]: [\"$exception_type\", \"$exception_source\", \"$exception_handled\", \"path\"],\n} as const satisfies Record<AutoEventName, readonly string[]>;\n\n/**\n * `$pageleave.scroll_depth` is the exact 0–100 percentage; `scroll_depth_bucket`\n * is the deepest of these thresholds reached, as a string — the value to\n * break down by. `\"0\"` means the visitor never reached the first threshold.\n */\nexport const SCROLL_DEPTH_BUCKETS = [25, 50, 75, 100] as const;\nexport type ScrollDepthBucket = \"0\" | `${(typeof SCROLL_DEPTH_BUCKETS)[number]}`;\n\nexport function scrollDepthBucket(depth: number): ScrollDepthBucket {\n let reached: ScrollDepthBucket = \"0\";\n for (const threshold of SCROLL_DEPTH_BUCKETS) {\n if (depth >= threshold) reached = `${threshold}`;\n }\n return reached;\n}\n"]}
|
|
@@ -1,16 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IDENTITY_ID_PATTERN
|
|
3
|
+
} from "./chunk-6ZJOC4NJ.js";
|
|
4
|
+
import {
|
|
5
|
+
RESERVED_EVENT_NAMES
|
|
6
|
+
} from "./chunk-FP2X3PRK.js";
|
|
7
|
+
|
|
1
8
|
// src/ingest.ts
|
|
2
9
|
import { z } from "zod";
|
|
3
|
-
var RESERVED_EVENT_NAMES = ["session_start", "session_end", "screen_view"];
|
|
4
10
|
var MAX_TRACK_PAYLOAD_BYTES = 512e3;
|
|
5
11
|
var zClientEventMetadata = {
|
|
6
12
|
/**
|
|
7
|
-
* Client-generated UUID. This is the idempotency key
|
|
8
|
-
* `RawEvent
|
|
9
|
-
*
|
|
13
|
+
* Client-generated UUID. This is the project-scoped idempotency key stored as
|
|
14
|
+
* `RawEvent.clientEventId`; the server uses a separate globally unique raw
|
|
15
|
+
* event id internally. Re-sending the same eventId to the same project is
|
|
16
|
+
* always safe and always reported as a duplicate.
|
|
10
17
|
*/
|
|
11
18
|
eventId: z.uuid(),
|
|
12
19
|
/** Event time, epoch milliseconds. */
|
|
13
|
-
timestamp: z.number().int().nonnegative()
|
|
20
|
+
timestamp: z.number().int().nonnegative(),
|
|
21
|
+
/**
|
|
22
|
+
* The browser device this event belongs to, for events a server records
|
|
23
|
+
* on a visitor's behalf (a form submission, an order). Lets one server
|
|
24
|
+
* batch carry events from many visitors — a forwarder cannot split its
|
|
25
|
+
* batches per device without breaking its failure model and its
|
|
26
|
+
* `accepted + duplicates + rejected === sent` invariant.
|
|
27
|
+
*
|
|
28
|
+
* Honoured in `server` mode only, where it takes precedence over the
|
|
29
|
+
* `reopt-device-id` request header. Ignored in `browser` mode: there the
|
|
30
|
+
* header is authoritative and a body field would be trivially forged.
|
|
31
|
+
* Same character set as every identity id.
|
|
32
|
+
*/
|
|
33
|
+
deviceId: z.string().regex(IDENTITY_ID_PATTERN, "expected an identity id").optional()
|
|
14
34
|
};
|
|
15
35
|
var zTrackPayload = z.object({
|
|
16
36
|
name: z.string().min(1).max(200),
|
|
@@ -130,7 +150,6 @@ var zIngestError = z.object({
|
|
|
130
150
|
});
|
|
131
151
|
|
|
132
152
|
export {
|
|
133
|
-
RESERVED_EVENT_NAMES,
|
|
134
153
|
MAX_TRACK_PAYLOAD_BYTES,
|
|
135
154
|
zTrackPayload,
|
|
136
155
|
zIdentifyPayload,
|
|
@@ -150,4 +169,4 @@ export {
|
|
|
150
169
|
zIngestErrorCode,
|
|
151
170
|
zIngestError
|
|
152
171
|
};
|
|
153
|
-
//# sourceMappingURL=chunk-
|
|
172
|
+
//# sourceMappingURL=chunk-5BRCHDS3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ingest.ts"],"sourcesContent":["/**\n * `POST /api/track` — the ingest contract.\n *\n * The request shape is shared by both credential modes. The *response* shape\n * is where server-ingest differs: it reports per-event rejections instead of\n * failing a whole batch, because a forwarder that stalls on one malformed row\n * stops forwarding forever.\n *\n * Absorbed from the former internal `@reopt/ingest-contract`; that package is\n * now a deprecated re-export of this module.\n */\nimport { z } from \"zod\";\nimport { IDENTITY_ID_PATTERN } from \"./identity.js\";\n\nexport {\n AUTO_EVENT_NAMES,\n AUTO_EVENT_PROPERTIES,\n AUTO_EVENT_ROLLUP_KEYS,\n RESERVED_EVENT_NAMES,\n SCROLL_DEPTH_BUCKETS,\n scrollDepthBucket,\n type AutoEventName,\n type ScrollDepthBucket,\n} from \"./events.js\";\nimport { RESERVED_EVENT_NAMES } from \"./events.js\";\n\n/** Hard cap enforced by the route before the body is even parsed. */\nexport const MAX_TRACK_PAYLOAD_BYTES = 512_000;\n\nconst zClientEventMetadata = {\n /**\n * Client-generated UUID. This is the project-scoped idempotency key stored as\n * `RawEvent.clientEventId`; the server uses a separate globally unique raw\n * event id internally. Re-sending the same eventId to the same project is\n * always safe and always reported as a duplicate.\n */\n eventId: z.uuid(),\n /** Event time, epoch milliseconds. */\n timestamp: z.number().int().nonnegative(),\n /**\n * The browser device this event belongs to, for events a server records\n * on a visitor's behalf (a form submission, an order). Lets one server\n * batch carry events from many visitors — a forwarder cannot split its\n * batches per device without breaking its failure model and its\n * `accepted + duplicates + rejected === sent` invariant.\n *\n * Honoured in `server` mode only, where it takes precedence over the\n * `reopt-device-id` request header. Ignored in `browser` mode: there the\n * header is authoritative and a body field would be trivially forged.\n * Same character set as every identity id.\n */\n deviceId: z.string().regex(IDENTITY_ID_PATTERN, \"expected an identity id\").optional(),\n};\n\nexport const zTrackPayload = z\n .object({\n name: z.string().min(1).max(200),\n properties: z.record(z.string(), z.unknown()).optional(),\n profileId: z.string().max(500).or(z.number()).optional(),\n })\n .refine((data) => !RESERVED_EVENT_NAMES.includes(data.name as never), {\n message: `Event name cannot be one of the reserved names: ${RESERVED_EVENT_NAMES.join(\", \")}`,\n path: [\"name\"],\n });\n\nexport const zIdentifyPayload = z.object({\n profileId: z.string().min(1).max(500).or(z.number()),\n firstName: z.string().max(200).optional(),\n lastName: z.string().max(200).optional(),\n email: z.email().max(320).optional(),\n avatar: z.url().max(2000).optional(),\n properties: z.record(z.string(), z.unknown()).optional(),\n});\n\nexport const zIncrementPayload = z.object({\n profileId: z.string().min(1).max(500).or(z.number()),\n property: z.string().min(1).max(200),\n value: z.number().positive().optional(),\n});\n\nexport const zDecrementPayload = z.object({\n profileId: z.string().min(1).max(500).or(z.number()),\n property: z.string().min(1).max(200),\n value: z.number().positive().optional(),\n});\n\nexport const zTrackHandlerPayload = z.discriminatedUnion(\"type\", [\n z.object({ type: z.literal(\"track\"), payload: zTrackPayload, ...zClientEventMetadata }),\n z.object({ type: z.literal(\"identify\"), payload: zIdentifyPayload, ...zClientEventMetadata }),\n z.object({ type: z.literal(\"increment\"), payload: zIncrementPayload, ...zClientEventMetadata }),\n z.object({ type: z.literal(\"decrement\"), payload: zDecrementPayload, ...zClientEventMetadata }),\n]);\n\nexport type ITrackPayload = z.infer<typeof zTrackPayload>;\nexport type IIdentifyPayload = z.infer<typeof zIdentifyPayload>;\nexport type IIncrementPayload = z.infer<typeof zIncrementPayload>;\nexport type IDecrementPayload = z.infer<typeof zDecrementPayload>;\nexport type ITrackHandlerPayload = z.infer<typeof zTrackHandlerPayload>;\n\n// ─── Ingest mode ────────────────────────────────────────────────────────────\n\n/**\n * Decided by the credential presented, never by a header the caller sets:\n * `reopt-write-key` alone → `browser`, `reopt-client-id` + `reopt-client-secret`\n * → `server`. A separate mode header could disagree with the credential; this\n * cannot.\n *\n * `browser` keeps the 5-second click dedup and device-scoped sessions.\n * `server` drops both and relies solely on `eventId` idempotency — a server\n * batch legitimately contains the same event name many times within one second,\n * and its \"device\" is a process, not a person.\n */\nexport const INGEST_MODES = [\"browser\", \"server\"] as const;\nexport const zIngestMode = z.enum(INGEST_MODES);\nexport type IngestMode = z.infer<typeof zIngestMode>;\n\n// ─── Response ───────────────────────────────────────────────────────────────\n\n/**\n * Why a single event in a batch was dropped. These are permanent: re-sending\n * the same row produces the same rejection, so a forwarder should count it as\n * skipped and advance its cursor rather than retry.\n */\nexport const INGEST_REJECTION_REASONS = [\n /** Failed the zod schema (bad eventId, missing name, wrong types…). */\n \"validation_failed\",\n /** Used one of RESERVED_EVENT_NAMES. */\n \"reserved_name\",\n /** Another row earlier in the same batch already claimed this eventId. */\n \"duplicate_in_batch\",\n] as const;\nexport const zIngestRejectionReason = z.enum(INGEST_REJECTION_REASONS);\nexport type IngestRejectionReason = z.infer<typeof zIngestRejectionReason>;\n\nexport const zIngestRejection = z.object({\n /** May be absent or malformed on `validation_failed` — hence `string`, not `uuid`. */\n eventId: z.string(),\n /** Position in the submitted batch. The only reliable identifier when eventId itself is bad. */\n index: z.number().int().nonnegative(),\n reason: zIngestRejectionReason,\n message: z.string().optional(),\n});\nexport type IngestRejection = z.infer<typeof zIngestRejection>;\n\nconst ingestCounts = {\n /** Raw events newly persisted by this request. */\n accepted: z.number().int().nonnegative(),\n /**\n * Events the server already had. In `server` mode this is exactly the\n * eventId-idempotency count; in `browser` mode it also includes the\n * 5-second click dedup.\n */\n duplicates: z.number().int().nonnegative(),\n /** Always `[]` in `browser` mode, where a bad row fails the whole batch. */\n rejected: z.array(zIngestRejection),\n};\n\n/** 200 — accepted and handed to the materialize queue. */\nexport const zIngestOkResponse = z.object({\n status: z.literal(\"ok\"),\n requestId: z.string(),\n mode: zIngestMode,\n ...ingestCounts,\n queued: z.boolean(),\n});\n\n/**\n * 202 — raw events landed, but enqueueing the materialize task failed. No data\n * is lost: the `replay-unmaterialized` cron picks these up. A forwarder should\n * treat this as success and advance its cursor. Carries the same counts as 200\n * so the reconciliation below works here too.\n */\nexport const zIngestAcceptedResponse = z.object({\n status: z.literal(\"accepted\"),\n requestId: z.string(),\n mode: zIngestMode,\n ...ingestCounts,\n backgroundQueued: z.literal(false),\n replay: z.literal(\"unmaterialized-raw-events\"),\n});\n\nexport const zIngestResponse = z.discriminatedUnion(\"status\", [zIngestOkResponse, zIngestAcceptedResponse]);\n\nexport type IngestOkResponse = z.infer<typeof zIngestOkResponse>;\nexport type IngestAcceptedResponse = z.infer<typeof zIngestAcceptedResponse>;\nexport type IngestResponse = z.infer<typeof zIngestResponse>;\n\n/**\n * The reconciliation a forwarder runs on every 2xx:\n *\n * accepted + duplicates + rejected.length === events sent\n *\n * A mismatch means the server silently dropped something, which is exactly the\n * failure mode this contract exists to make impossible to miss.\n */\nexport function reconcileIngestResponse(response: IngestResponse, sentCount: number): boolean {\n return response.accepted + response.duplicates + response.rejected.length === sentCount;\n}\n\n// ─── Errors ─────────────────────────────────────────────────────────────────\n\n/**\n * Stable machine-readable codes. Branch on these, never on `error`/`message`,\n * which are prose and may be reworded.\n */\nexport const INGEST_ERROR_CODES = [\n /** 401 — unknown or mismatched credentials. Stop and alert; do not retry. */\n \"unauthorized\",\n /** 400 — body was not valid JSON. */\n \"invalid_json\",\n /** 400 — request exceeded MAX_TRACK_PAYLOAD_BYTES. Split the batch. */\n \"payload_too_large\",\n /** 400 — zero events submitted. */\n \"empty_batch\",\n /** 400 — batch contained a `type: \"alias\"` event, which is not supported. */\n \"alias_unsupported\",\n /** 400 — the project has no organization; it cannot be billed or quota-checked. */\n \"project_without_organization\",\n /** 400 — `browser` mode only: a row failed validation. `server` mode reports these in `rejected[]`. */\n \"validation_failed\",\n /** 429 — per-project request rate limit. `Retry-After` set. Pause this tick, keep the cursor. */\n \"rate_limited\",\n /** 429 — the organization's monthly event quota is exhausted. `Retry-After` set (capped at 1h). Alert: a human must raise the limit. */\n \"quota_exceeded\",\n /** 410 — the project is being deleted. Permanent; stop sending. */\n \"project_purging\",\n /** 500 — unexpected server failure. Back off and retry. */\n \"internal_error\",\n] as const;\nexport const zIngestErrorCode = z.enum(INGEST_ERROR_CODES);\nexport type IngestErrorCode = z.infer<typeof zIngestErrorCode>;\n\nexport const zIngestError = z.object({\n status: z.number().int(),\n code: zIngestErrorCode,\n error: z.string(),\n message: z.string().optional(),\n /** zod issues, when `code === \"validation_failed\"`. */\n errors: z.unknown().optional(),\n requestId: z.string().optional(),\n});\nexport type IngestError = z.infer<typeof zIngestError>;\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEVICE_ID_HEADER
|
|
3
|
+
} from "./chunk-IXPP5K4I.js";
|
|
4
|
+
|
|
5
|
+
// src/identity.ts
|
|
6
|
+
var SESSION_ID_HEADER = "reopt-session-id";
|
|
7
|
+
var IDENTITY_REQUEST_HEADERS = [DEVICE_ID_HEADER, SESSION_ID_HEADER];
|
|
8
|
+
var COOKIE_MAX_AGE_SECONDS = 400 * 24 * 60 * 60;
|
|
9
|
+
var OPT_OUT_CONSENT_CATEGORY = "analytics";
|
|
10
|
+
var COOKIE_PREFIX = "reopt_";
|
|
11
|
+
function cookieSafe(writeKey) {
|
|
12
|
+
if (!writeKey) {
|
|
13
|
+
throw new Error("[reopt] writeKey is required to derive cookie names");
|
|
14
|
+
}
|
|
15
|
+
return writeKey.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
16
|
+
}
|
|
17
|
+
function deviceCookieName(writeKey) {
|
|
18
|
+
return `${COOKIE_PREFIX}${cookieSafe(writeKey)}_device`;
|
|
19
|
+
}
|
|
20
|
+
function consentCookieName(writeKey) {
|
|
21
|
+
return `${COOKIE_PREFIX}${cookieSafe(writeKey)}_consent`;
|
|
22
|
+
}
|
|
23
|
+
var IDENTITY_ID_PATTERN = /^[A-Za-z0-9._~-]{1,200}$/;
|
|
24
|
+
function isValidIdentityId(value) {
|
|
25
|
+
return typeof value === "string" && IDENTITY_ID_PATTERN.test(value);
|
|
26
|
+
}
|
|
27
|
+
function decode(raw) {
|
|
28
|
+
try {
|
|
29
|
+
return decodeURIComponent(raw);
|
|
30
|
+
} catch {
|
|
31
|
+
return raw;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function parseDeviceCookie(raw) {
|
|
35
|
+
if (!raw) return null;
|
|
36
|
+
const text = decode(raw.trim());
|
|
37
|
+
if (!text) return null;
|
|
38
|
+
if (!text.startsWith("{")) {
|
|
39
|
+
return isValidIdentityId(text) ? { deviceId: text } : null;
|
|
40
|
+
}
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(text);
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
48
|
+
const candidate = parsed;
|
|
49
|
+
if (!isValidIdentityId(candidate.deviceId)) return null;
|
|
50
|
+
const state = { deviceId: candidate.deviceId };
|
|
51
|
+
if (isValidIdentityId(candidate.sessionId)) state.sessionId = candidate.sessionId;
|
|
52
|
+
if (isValidIdentityId(candidate.profileId)) state.profileId = candidate.profileId;
|
|
53
|
+
return state;
|
|
54
|
+
}
|
|
55
|
+
function serializeDeviceCookie(state) {
|
|
56
|
+
if (!isValidIdentityId(state.deviceId)) {
|
|
57
|
+
throw new Error("[reopt] deviceId must be a non-empty string without control characters");
|
|
58
|
+
}
|
|
59
|
+
const envelope = { deviceId: state.deviceId };
|
|
60
|
+
if (isValidIdentityId(state.sessionId)) envelope.sessionId = state.sessionId;
|
|
61
|
+
if (isValidIdentityId(state.profileId)) envelope.profileId = state.profileId;
|
|
62
|
+
return encodeURIComponent(JSON.stringify(envelope));
|
|
63
|
+
}
|
|
64
|
+
function parseConsentCookie(raw) {
|
|
65
|
+
if (!raw) return null;
|
|
66
|
+
const text = decode(raw.trim());
|
|
67
|
+
if (!text.startsWith("{")) return null;
|
|
68
|
+
let parsed;
|
|
69
|
+
try {
|
|
70
|
+
parsed = JSON.parse(text);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
75
|
+
const state = {};
|
|
76
|
+
for (const [category, decision] of Object.entries(parsed)) {
|
|
77
|
+
if (typeof decision === "boolean" && isValidIdentityId(category)) {
|
|
78
|
+
state[category] = decision;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return state;
|
|
82
|
+
}
|
|
83
|
+
function serializeConsentCookie(state) {
|
|
84
|
+
const clean = {};
|
|
85
|
+
for (const [category, decision] of Object.entries(state)) {
|
|
86
|
+
if (typeof decision === "boolean" && isValidIdentityId(category)) {
|
|
87
|
+
clean[category] = decision;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return encodeURIComponent(JSON.stringify(clean));
|
|
91
|
+
}
|
|
92
|
+
function isOptedOut(consent) {
|
|
93
|
+
return consent?.[OPT_OUT_CONSENT_CATEGORY] === false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
SESSION_ID_HEADER,
|
|
98
|
+
IDENTITY_REQUEST_HEADERS,
|
|
99
|
+
COOKIE_MAX_AGE_SECONDS,
|
|
100
|
+
OPT_OUT_CONSENT_CATEGORY,
|
|
101
|
+
deviceCookieName,
|
|
102
|
+
consentCookieName,
|
|
103
|
+
IDENTITY_ID_PATTERN,
|
|
104
|
+
isValidIdentityId,
|
|
105
|
+
parseDeviceCookie,
|
|
106
|
+
serializeDeviceCookie,
|
|
107
|
+
parseConsentCookie,
|
|
108
|
+
serializeConsentCookie,
|
|
109
|
+
isOptedOut
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=chunk-6ZJOC4NJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/identity.ts"],"sourcesContent":["/**\n * `@reopt-ai/data-contract/identity` — the names and encodings that carry a\n * visitor's identity between the browser SDK, the server SDK, the Next.js\n * proxy and the ingest pipeline.\n *\n * Every one of those parties reads or writes the same cookies and headers.\n * None of them depends on another (ingest does not import the SDKs), so this\n * module is the only place a name may be spelled out. A party that hardcodes\n * `reopt_<key>_device` instead of calling `deviceCookieName()` fails silently:\n * the browser sets one cookie, the server looks for another, and every\n * visitor is counted as new. `packages/db/src/__tests__/redis-namespace.test.ts`\n * guards Redis key prefixes for exactly the same reason.\n *\n * Nothing here touches `document`, `fetch` or Node built-ins — it runs in\n * browsers, edge runtimes and Node alike.\n */\nimport { DEVICE_ID_HEADER } from \"./version.js\";\n\nexport {\n CLIENT_ID_HEADER,\n CLIENT_SECRET_HEADER,\n CONTRACT_VERSION,\n CONTRACT_VERSION_HEADER,\n DEVICE_ID_HEADER,\n REQUEST_ID_HEADER,\n WRITE_KEY_HEADER,\n} from \"./version.js\";\n\n/**\n * Request header carrying the browser's current session id. The browser SDK\n * adds it to same-origin requests so a server-side `track()` can land in the\n * session the user is actually in; the server SDK forwards it to ingest.\n *\n * Ingest treats it as a *hint*, never as authority — a browser can put any\n * value here.\n */\nexport const SESSION_ID_HEADER = \"reopt-session-id\";\n\n/** Every request header the identity protocol may carry, for CORS allow-lists. */\nexport const IDENTITY_REQUEST_HEADERS = [DEVICE_ID_HEADER, SESSION_ID_HEADER] as const;\n\n/**\n * 400 days — the longest lifetime Chrome will honour. Anything longer is\n * silently clamped to this, so asking for more only hides the real expiry.\n */\nexport const COOKIE_MAX_AGE_SECONDS = 400 * 24 * 60 * 60;\n\n/** Consent category whose refusal means \"send nothing at all\". */\nexport const OPT_OUT_CONSENT_CATEGORY = \"analytics\";\n\nconst COOKIE_PREFIX = \"reopt_\";\n\n/**\n * Cookie names may not contain separators (`=`, `;`, `,`, whitespace) or\n * control characters. A write key is opaque to us, so anything outside the\n * safe set is folded to `_` — deterministically, so both ends agree.\n */\nfunction cookieSafe(writeKey: string): string {\n if (!writeKey) {\n throw new Error(\"[reopt] writeKey is required to derive cookie names\");\n }\n return writeKey.replace(/[^A-Za-z0-9_-]/g, \"_\");\n}\n\n/** `reopt_<writeKey>_device` — device id plus session/profile hints. */\nexport function deviceCookieName(writeKey: string): string {\n return `${COOKIE_PREFIX}${cookieSafe(writeKey)}_device`;\n}\n\n/** `reopt_<writeKey>_consent` — per-category consent decisions. */\nexport function consentCookieName(writeKey: string): string {\n return `${COOKIE_PREFIX}${cookieSafe(writeKey)}_consent`;\n}\n\n/** Everything the device cookie may carry. Only `deviceId` is required. */\nexport interface DeviceCookieState {\n deviceId: string;\n /** Current session, if the browser has one. */\n sessionId?: string;\n /** Last identified profile. A hint for the server SDK, never trusted by ingest. */\n profileId?: string;\n}\n\n/** Per-category consent. `false` withdraws, `true` grants, absent = undecided. */\nexport type ConsentCookieState = Record<string, boolean>;\n\n/**\n * What the server hands the browser SDK so the first render already agrees\n * with the server about who the visitor is. Plain JSON — it crosses the\n * RSC → client component boundary as a prop.\n */\nexport interface ReoptBootstrap {\n /** From the device cookie (seeded by the proxy on first visit). */\n deviceId: string;\n /** Session hint, or `null` when the browser has not started one. */\n sessionId: string | null;\n /** Result of the server's `getProfileId` resolver, or `null`. */\n profileId: string | null;\n consent: ConsentCookieState;\n /** Server clock at render time, so a skewed device clock can be corrected. */\n serverTimeMs: number;\n}\n\n/**\n * Ids are opaque, but a cookie is attacker-controlled input (a sibling\n * subdomain can set one). The value ends up in an HTTP header, and\n * `Headers.set` throws on anything outside ISO-8859-1 — so a single odd\n * cookie would break every `fetch` the tracing patch touches. Only the URL\n * \"unreserved\" set is accepted; every id this SDK mints is a UUID.\n */\nexport const IDENTITY_ID_PATTERN = /^[A-Za-z0-9._~-]{1,200}$/;\n\nexport function isValidIdentityId(value: unknown): value is string {\n return typeof value === \"string\" && IDENTITY_ID_PATTERN.test(value);\n}\n\nfunction decode(raw: string): string {\n try {\n return decodeURIComponent(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Reads a device cookie value. Accepts the JSON envelope written by\n * `serializeDeviceCookie` and, for cookies seeded before the envelope existed,\n * a bare device id. Returns `null` for anything else — a malformed cookie is\n * treated as absent, never as an error, so a corrupted value cannot take the\n * page down.\n */\nexport function parseDeviceCookie(raw: string | null | undefined): DeviceCookieState | null {\n if (!raw) return null;\n const text = decode(raw.trim());\n if (!text) return null;\n\n if (!text.startsWith(\"{\")) {\n return isValidIdentityId(text) ? { deviceId: text } : null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return null;\n }\n if (typeof parsed !== \"object\" || parsed === null) return null;\n\n const candidate = parsed as Record<string, unknown>;\n if (!isValidIdentityId(candidate.deviceId)) return null;\n\n const state: DeviceCookieState = { deviceId: candidate.deviceId };\n if (isValidIdentityId(candidate.sessionId)) state.sessionId = candidate.sessionId;\n if (isValidIdentityId(candidate.profileId)) state.profileId = candidate.profileId;\n return state;\n}\n\n/**\n * Cookie values may not contain `;`, `,` or whitespace, and JSON contains\n * quotes and commas. The envelope is therefore URI-encoded; `parseDeviceCookie`\n * reverses it. Optional fields are omitted rather than written as `null` so\n * the cookie stays as small as the state it carries.\n */\nexport function serializeDeviceCookie(state: DeviceCookieState): string {\n if (!isValidIdentityId(state.deviceId)) {\n throw new Error(\"[reopt] deviceId must be a non-empty string without control characters\");\n }\n const envelope: DeviceCookieState = { deviceId: state.deviceId };\n if (isValidIdentityId(state.sessionId)) envelope.sessionId = state.sessionId;\n if (isValidIdentityId(state.profileId)) envelope.profileId = state.profileId;\n return encodeURIComponent(JSON.stringify(envelope));\n}\n\n/**\n * Reads a consent cookie. Only boolean entries survive; anything else in the\n * object is dropped rather than failing the whole cookie, because one odd key\n * must not erase a user's recorded opt-out.\n */\nexport function parseConsentCookie(raw: string | null | undefined): ConsentCookieState | null {\n if (!raw) return null;\n const text = decode(raw.trim());\n if (!text.startsWith(\"{\")) return null;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return null;\n }\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) return null;\n\n const state: ConsentCookieState = {};\n for (const [category, decision] of Object.entries(parsed as Record<string, unknown>)) {\n if (typeof decision === \"boolean\" && isValidIdentityId(category)) {\n state[category] = decision;\n }\n }\n return state;\n}\n\nexport function serializeConsentCookie(state: ConsentCookieState): string {\n const clean: ConsentCookieState = {};\n for (const [category, decision] of Object.entries(state)) {\n if (typeof decision === \"boolean\" && isValidIdentityId(category)) {\n clean[category] = decision;\n }\n }\n return encodeURIComponent(JSON.stringify(clean));\n}\n\n/**\n * \"Opted out\" means the visitor refused the `analytics` category. An absent\n * cookie or an undecided category is *not* an opt-out — it is the default\n * state of every first visit, and treating it as refusal would make consent\n * banners mandatory for every integration.\n */\nexport function isOptedOut(consent: ConsentCookieState | null | undefined): boolean {\n return consent?.[OPT_OUT_CONSENT_CATEGORY] === false;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/events.ts
|
|
2
|
+
var RESERVED_EVENT_NAMES = ["session_start", "session_end", "screen_view"];
|
|
3
|
+
var AUTO_EVENT_NAMES = {
|
|
4
|
+
pageview: "$pageview",
|
|
5
|
+
screenView: "$screen_view",
|
|
6
|
+
pageleave: "$pageleave",
|
|
7
|
+
webVitals: "$web_vitals",
|
|
8
|
+
exception: "$exception"
|
|
9
|
+
};
|
|
10
|
+
var AUTO_EVENT_PROPERTIES = {
|
|
11
|
+
[AUTO_EVENT_NAMES.pageview]: [
|
|
12
|
+
"path",
|
|
13
|
+
"origin",
|
|
14
|
+
"title",
|
|
15
|
+
"referrer",
|
|
16
|
+
"search",
|
|
17
|
+
"utm_source",
|
|
18
|
+
"utm_medium",
|
|
19
|
+
"utm_campaign",
|
|
20
|
+
"utm_term",
|
|
21
|
+
"utm_content"
|
|
22
|
+
],
|
|
23
|
+
[AUTO_EVENT_NAMES.screenView]: ["screen_name"],
|
|
24
|
+
[AUTO_EVENT_NAMES.pageleave]: ["path", "origin", "duration", "scroll_depth", "scroll_depth_bucket"],
|
|
25
|
+
[AUTO_EVENT_NAMES.webVitals]: ["metric_name", "metric_id", "value", "delta", "rating", "navigation_type", "path"],
|
|
26
|
+
// `$`-prefixed on purpose: these are reserved, SDK-owned keys with fixed
|
|
27
|
+
// semantics, distinguished from a host's own snake_case vocabulary the
|
|
28
|
+
// same way the event names are. Not a typo — do not strip the prefix.
|
|
29
|
+
[AUTO_EVENT_NAMES.exception]: [
|
|
30
|
+
"$exception_type",
|
|
31
|
+
"$exception_message",
|
|
32
|
+
"$exception_stack",
|
|
33
|
+
"$exception_source",
|
|
34
|
+
"$exception_handled",
|
|
35
|
+
"path"
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
var AUTO_EVENT_ROLLUP_KEYS = {
|
|
39
|
+
[AUTO_EVENT_NAMES.pageview]: ["path", "origin", "referrer", "utm_source", "utm_medium", "utm_campaign"],
|
|
40
|
+
[AUTO_EVENT_NAMES.screenView]: ["screen_name"],
|
|
41
|
+
[AUTO_EVENT_NAMES.pageleave]: ["path", "origin", "scroll_depth_bucket"],
|
|
42
|
+
[AUTO_EVENT_NAMES.webVitals]: ["metric_name", "rating", "navigation_type", "path"],
|
|
43
|
+
[AUTO_EVENT_NAMES.exception]: ["$exception_type", "$exception_source", "$exception_handled", "path"]
|
|
44
|
+
};
|
|
45
|
+
var SCROLL_DEPTH_BUCKETS = [25, 50, 75, 100];
|
|
46
|
+
function scrollDepthBucket(depth) {
|
|
47
|
+
let reached = "0";
|
|
48
|
+
for (const threshold of SCROLL_DEPTH_BUCKETS) {
|
|
49
|
+
if (depth >= threshold) reached = `${threshold}`;
|
|
50
|
+
}
|
|
51
|
+
return reached;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
RESERVED_EVENT_NAMES,
|
|
56
|
+
AUTO_EVENT_NAMES,
|
|
57
|
+
AUTO_EVENT_PROPERTIES,
|
|
58
|
+
AUTO_EVENT_ROLLUP_KEYS,
|
|
59
|
+
SCROLL_DEPTH_BUCKETS,
|
|
60
|
+
scrollDepthBucket
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=chunk-FP2X3PRK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/events.ts"],"sourcesContent":["/**\n * `@reopt-ai/data-contract/events` — event names and property keys, with no\n * zod. Browser SDKs import this so their production bundles stay free of\n * schema code; `./ingest` re-exports everything here.\n */\n\n/** Event names the pipeline reserves for its own session bookkeeping. */\nexport const RESERVED_EVENT_NAMES = [\"session_start\", \"session_end\", \"screen_view\"] as const;\n\n/**\n * Events the SDKs emit on their own. One declaration, so a consumer that\n * registers them in its event catalogue (to make them roll up) cannot drift\n * from what the SDK actually sends — a drifted name silently falls back to\n * raw queries, which is slower but not visibly wrong.\n */\nexport const AUTO_EVENT_NAMES = {\n pageview: \"$pageview\",\n screenView: \"$screen_view\",\n pageleave: \"$pageleave\",\n webVitals: \"$web_vitals\",\n exception: \"$exception\",\n} as const;\nexport type AutoEventName = (typeof AUTO_EVENT_NAMES)[keyof typeof AUTO_EVENT_NAMES];\n\n/**\n * Property keys each automatic event carries. Consumers derive their\n * breakdown / rollup property lists from here rather than retyping them.\n * `path`, `origin`, `referrer` and `utm_*` are the keys the ingest pipeline\n * reads into session and event columns.\n */\nexport const AUTO_EVENT_PROPERTIES = {\n [AUTO_EVENT_NAMES.pageview]: [\n \"path\",\n \"origin\",\n \"title\",\n \"referrer\",\n \"search\",\n \"utm_source\",\n \"utm_medium\",\n \"utm_campaign\",\n \"utm_term\",\n \"utm_content\",\n ],\n [AUTO_EVENT_NAMES.screenView]: [\"screen_name\"],\n [AUTO_EVENT_NAMES.pageleave]: [\"path\", \"origin\", \"duration\", \"scroll_depth\", \"scroll_depth_bucket\"],\n [AUTO_EVENT_NAMES.webVitals]: [\"metric_name\", \"metric_id\", \"value\", \"delta\", \"rating\", \"navigation_type\", \"path\"],\n // `$`-prefixed on purpose: these are reserved, SDK-owned keys with fixed\n // semantics, distinguished from a host's own snake_case vocabulary the\n // same way the event names are. Not a typo — do not strip the prefix.\n [AUTO_EVENT_NAMES.exception]: [\n \"$exception_type\",\n \"$exception_message\",\n \"$exception_stack\",\n \"$exception_source\",\n \"$exception_handled\",\n \"path\",\n ],\n} as const satisfies Record<AutoEventName, readonly string[]>;\n\n/**\n * The subset of {@link AUTO_EVENT_PROPERTIES} that is a sensible rollup\n * dimension: low cardinality, categorical. Per-measurement ids and\n * continuous values (`metric_id`, `value`, `delta`, `duration`,\n * `scroll_depth`) are deliberately absent — as dimensions they would make\n * the rollup larger than the raw table it summarises. Derive\n * `rollupProperties` from here, not from the full property list.\n */\nexport const AUTO_EVENT_ROLLUP_KEYS = {\n [AUTO_EVENT_NAMES.pageview]: [\"path\", \"origin\", \"referrer\", \"utm_source\", \"utm_medium\", \"utm_campaign\"],\n [AUTO_EVENT_NAMES.screenView]: [\"screen_name\"],\n [AUTO_EVENT_NAMES.pageleave]: [\"path\", \"origin\", \"scroll_depth_bucket\"],\n [AUTO_EVENT_NAMES.webVitals]: [\"metric_name\", \"rating\", \"navigation_type\", \"path\"],\n [AUTO_EVENT_NAMES.exception]: [\"$exception_type\", \"$exception_source\", \"$exception_handled\", \"path\"],\n} as const satisfies Record<AutoEventName, readonly string[]>;\n\n/**\n * `$pageleave.scroll_depth` is the exact 0–100 percentage; `scroll_depth_bucket`\n * is the deepest of these thresholds reached, as a string — the value to\n * break down by. `\"0\"` means the visitor never reached the first threshold.\n */\nexport const SCROLL_DEPTH_BUCKETS = [25, 50, 75, 100] as const;\nexport type ScrollDepthBucket = \"0\" | `${(typeof SCROLL_DEPTH_BUCKETS)[number]}`;\n\nexport function scrollDepthBucket(depth: number): ScrollDepthBucket {\n let reached: ScrollDepthBucket = \"0\";\n for (const threshold of SCROLL_DEPTH_BUCKETS) {\n if (depth >= threshold) reached = `${threshold}`;\n }\n return reached;\n}\n"],"mappings":";AAOO,IAAM,uBAAuB,CAAC,iBAAiB,eAAe,aAAa;AAQ3E,IAAM,mBAAmB;AAAA,EAC9B,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,WAAW;AAAA,EACX,WAAW;AACb;AASO,IAAM,wBAAwB;AAAA,EACnC,CAAC,iBAAiB,QAAQ,GAAG;AAAA,IAC3B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,CAAC,iBAAiB,UAAU,GAAG,CAAC,aAAa;AAAA,EAC7C,CAAC,iBAAiB,SAAS,GAAG,CAAC,QAAQ,UAAU,YAAY,gBAAgB,qBAAqB;AAAA,EAClG,CAAC,iBAAiB,SAAS,GAAG,CAAC,eAAe,aAAa,SAAS,SAAS,UAAU,mBAAmB,MAAM;AAAA;AAAA;AAAA;AAAA,EAIhH,CAAC,iBAAiB,SAAS,GAAG;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAUO,IAAM,yBAAyB;AAAA,EACpC,CAAC,iBAAiB,QAAQ,GAAG,CAAC,QAAQ,UAAU,YAAY,cAAc,cAAc,cAAc;AAAA,EACtG,CAAC,iBAAiB,UAAU,GAAG,CAAC,aAAa;AAAA,EAC7C,CAAC,iBAAiB,SAAS,GAAG,CAAC,QAAQ,UAAU,qBAAqB;AAAA,EACtE,CAAC,iBAAiB,SAAS,GAAG,CAAC,eAAe,UAAU,mBAAmB,MAAM;AAAA,EACjF,CAAC,iBAAiB,SAAS,GAAG,CAAC,mBAAmB,qBAAqB,sBAAsB,MAAM;AACrG;AAOO,IAAM,uBAAuB,CAAC,IAAI,IAAI,IAAI,GAAG;AAG7C,SAAS,kBAAkB,OAAkC;AAClE,MAAI,UAA6B;AACjC,aAAW,aAAa,sBAAsB;AAC5C,QAAI,SAAS,UAAW,WAAU,GAAG,SAAS;AAAA,EAChD;AACA,SAAO;AACT;","names":[]}
|