@reopt-ai/data-contract 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 +21 -0
- package/README.md +99 -0
- package/dist/chunk-DHSMXB54.cjs +245 -0
- package/dist/chunk-DHSMXB54.cjs.map +1 -0
- package/dist/chunk-FRJK7OCQ.js +151 -0
- package/dist/chunk-FRJK7OCQ.js.map +1 -0
- package/dist/chunk-HRKELGGH.cjs +151 -0
- package/dist/chunk-HRKELGGH.cjs.map +1 -0
- package/dist/chunk-IAOPUBJU.js +61 -0
- package/dist/chunk-IAOPUBJU.js.map +1 -0
- package/dist/chunk-SJYY42BP.js +245 -0
- package/dist/chunk-SJYY42BP.js.map +1 -0
- package/dist/chunk-ZQVRSCTW.cjs +61 -0
- package/dist/chunk-ZQVRSCTW.cjs.map +1 -0
- package/dist/client.cjs +205 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +59 -0
- package/dist/client.d.ts +59 -0
- package/dist/client.js +205 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +25 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +83 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest.cjs +43 -0
- package/dist/ingest.cjs.map +1 -0
- package/dist/ingest.d.cts +270 -0
- package/dist/ingest.d.ts +270 -0
- package/dist/ingest.js +43 -0
- package/dist/ingest.js.map +1 -0
- package/dist/query.cjs +59 -0
- package/dist/query.cjs.map +1 -0
- package/dist/query.d.cts +374 -0
- package/dist/query.d.ts +374 -0
- package/dist/query.js +59 -0
- package/dist/query.js.map +1 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Reopt
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# `@reopt-ai/data-contract`
|
|
2
|
+
|
|
3
|
+
The wire contract between reopt-data and its server-credential consumers: zod
|
|
4
|
+
schemas for the ingest and query planes, plus a typed fetch client. One runtime
|
|
5
|
+
dependency (zod), because consumers bundle this.
|
|
6
|
+
|
|
7
|
+
Defined by [RFC-0023](../../docs/plans/2026-08-23-data-contract-v1-draft.md).
|
|
8
|
+
|
|
9
|
+
## Subpaths
|
|
10
|
+
|
|
11
|
+
| Import | Contents |
|
|
12
|
+
| -------------------------------- | ------------------------------------------------------------------- |
|
|
13
|
+
| `@reopt-ai/data-contract` | `CONTRACT_VERSION`, header names, `DataApiError`, shared primitives |
|
|
14
|
+
| `@reopt-ai/data-contract/ingest` | `POST /api/track` request + response schemas |
|
|
15
|
+
| `@reopt-ai/data-contract/query` | `POST /api/v1/query/*` request + response schemas |
|
|
16
|
+
| `@reopt-ai/data-contract/client` | `createDataClient()` |
|
|
17
|
+
|
|
18
|
+
Only `/client` references `fetch`. If you bring your own transport, import
|
|
19
|
+
`/query` alone and you take on no fetch surface.
|
|
20
|
+
|
|
21
|
+
## Ingest modes
|
|
22
|
+
|
|
23
|
+
The credential decides the mode; there is no mode header to disagree with it.
|
|
24
|
+
|
|
25
|
+
| | `browser` (`reopt-write-key`) | `server` (`reopt-client-id` + `reopt-client-secret`) |
|
|
26
|
+
| --------------------------- | ---------------------------------------------------- | ---------------------------------------------------- |
|
|
27
|
+
| 5-second click dedup | yes | no |
|
|
28
|
+
| Idempotency | `eventId` | `eventId` — the only mechanism |
|
|
29
|
+
| Device identity | `reopt-device-id` header, else generated per request | the event's `profileId`, else none |
|
|
30
|
+
| Sessions | per device | not created |
|
|
31
|
+
| A row that fails validation | fails the whole batch (400) | reported in `rejected[]`; the batch proceeds |
|
|
32
|
+
|
|
33
|
+
Server mode exists because a server batch legitimately repeats the same event
|
|
34
|
+
name many times per second, and its "device" is a process rather than a person.
|
|
35
|
+
Applying browser rules to it silently discards events and fabricates sessions.
|
|
36
|
+
|
|
37
|
+
## Reconciling a batch
|
|
38
|
+
|
|
39
|
+
Every 2xx satisfies:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
accepted + duplicates + rejected.length === events sent
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { reconcileIngestResponse } from "@reopt-ai/data-contract/ingest";
|
|
47
|
+
|
|
48
|
+
const response = await client.ingest.track(batch);
|
|
49
|
+
if (!reconcileIngestResponse(response, batch.length)) {
|
|
50
|
+
// The server dropped something without saying so — alert, do not advance.
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Rejections are permanent: the same row rejects the same way on resend. Count
|
|
55
|
+
them as skipped and move the cursor forward.
|
|
56
|
+
|
|
57
|
+
## Reading
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { createDataClient } from "@reopt-ai/data-contract/client";
|
|
61
|
+
|
|
62
|
+
const client = createDataClient({
|
|
63
|
+
baseUrl: process.env.REOPT_DATA_URL!,
|
|
64
|
+
clientId: process.env.REOPT_DATA_CLIENT_ID!,
|
|
65
|
+
clientSecret: process.env.REOPT_DATA_CLIENT_SECRET!,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const { data, meta } = await client.query.eventsTimeseries({
|
|
69
|
+
projectId,
|
|
70
|
+
startDate: "2026-08-01",
|
|
71
|
+
endDate: "2026-08-23",
|
|
72
|
+
granularity: "day",
|
|
73
|
+
eventName: "checkout_completed",
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`meta` reports staleness as two separate numbers, never one sum, because they
|
|
78
|
+
have different remedies:
|
|
79
|
+
|
|
80
|
+
- `cacheAgeSeconds` — how old the returned computation is. Resolves itself at
|
|
81
|
+
the next TTL.
|
|
82
|
+
- `ingestLagSeconds` — how far behind materialization was: the age of the oldest
|
|
83
|
+
raw event not yet materialized, or `0` when nothing is pending. A non-zero
|
|
84
|
+
value means the read model is genuinely behind, which is an "aggregating"
|
|
85
|
+
state, not a stale-cache state.
|
|
86
|
+
|
|
87
|
+
Not `now - max(events.created_at)`: that conflates a lagging pipeline with a
|
|
88
|
+
quiet project and grows without bound while a project sits idle.
|
|
89
|
+
|
|
90
|
+
## Errors
|
|
91
|
+
|
|
92
|
+
Non-2xx responses throw `DataApiError` with a stable `code`. Branch on `code`,
|
|
93
|
+
never on `error`/`message` — those are prose.
|
|
94
|
+
|
|
95
|
+
Note that the two 429s mean different things: `rate_limited` clears on its own,
|
|
96
|
+
`quota_exceeded` needs a human to raise the limit.
|
|
97
|
+
|
|
98
|
+
A 2xx whose body does not match the contract also throws, as
|
|
99
|
+
`contract_mismatch`, rather than being handed back half-understood.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
var _chunkZQVRSCTWcjs = require('./chunk-ZQVRSCTW.cjs');
|
|
5
|
+
|
|
6
|
+
// src/query.ts
|
|
7
|
+
var _zod = require('zod');
|
|
8
|
+
var zGranularity = _zod.z.enum(["hour", "day", "week", "month"]);
|
|
9
|
+
var zTimezone = _zod.z.string().min(1).max(64).refine(isValidTimeZone, { message: "unknown IANA time zone" });
|
|
10
|
+
function isValidTimeZone(value) {
|
|
11
|
+
try {
|
|
12
|
+
new Intl.DateTimeFormat("en-US", { timeZone: value });
|
|
13
|
+
return true;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
var zChannelFilter = _zod.z.object({
|
|
19
|
+
referrerTypes: _zod.z.array(_zod.z.string()).optional(),
|
|
20
|
+
channelTypes: _zod.z.array(_zod.z.string()).optional(),
|
|
21
|
+
utmSources: _zod.z.array(_zod.z.string()).optional(),
|
|
22
|
+
utmMediums: _zod.z.array(_zod.z.string()).optional(),
|
|
23
|
+
utmCampaigns: _zod.z.array(_zod.z.string()).optional()
|
|
24
|
+
}).optional();
|
|
25
|
+
var zQueryMeta = _zod.z.object({
|
|
26
|
+
projectId: _zod.z.string(),
|
|
27
|
+
/** When the underlying computation actually ran. On a cache hit, when the cached value was computed. */
|
|
28
|
+
computedAt: _chunkZQVRSCTWcjs.zInstant,
|
|
29
|
+
/** Age of the returned computation, in seconds. `0` on a cache miss. */
|
|
30
|
+
cacheAgeSeconds: _zod.z.number().int().nonnegative(),
|
|
31
|
+
/**
|
|
32
|
+
* How far behind materialization was when the computation ran, in seconds:
|
|
33
|
+
* the age of the oldest raw event not yet materialized, or `0` when nothing
|
|
34
|
+
* is pending.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately not `now - max(events.created_at)` — that conflates "the
|
|
37
|
+
* pipeline is behind" with "this project is quiet", and grows without bound
|
|
38
|
+
* on an idle project.
|
|
39
|
+
*/
|
|
40
|
+
ingestLagSeconds: _zod.z.number().int().nonnegative(),
|
|
41
|
+
cached: _zod.z.boolean(),
|
|
42
|
+
/**
|
|
43
|
+
* The time zone actually used to place bucket and window boundaries — the
|
|
44
|
+
* resolved value, not the requested one. Read it rather than assuming the
|
|
45
|
+
* request was honoured: when no zone is given anywhere it is `"UTC"`, and
|
|
46
|
+
* that is exactly the case a consumer needs to notice.
|
|
47
|
+
*/
|
|
48
|
+
timezone: _zod.z.string(),
|
|
49
|
+
requestId: _zod.z.string()
|
|
50
|
+
});
|
|
51
|
+
function zQueryEnvelope(data) {
|
|
52
|
+
return _zod.z.object({ data, meta: zQueryMeta });
|
|
53
|
+
}
|
|
54
|
+
var zTimeseriesBreakdown = _zod.z.object({
|
|
55
|
+
kind: _zod.z.literal("property"),
|
|
56
|
+
key: _zod.z.string().min(1).max(200),
|
|
57
|
+
topN: _zod.z.number().int().min(1).max(10).default(5)
|
|
58
|
+
});
|
|
59
|
+
var zEventsTimeseriesInput = _zod.z.object({
|
|
60
|
+
projectId: _zod.z.string().min(1),
|
|
61
|
+
startDate: _chunkZQVRSCTWcjs.zDate,
|
|
62
|
+
endDate: _chunkZQVRSCTWcjs.zDate,
|
|
63
|
+
granularity: zGranularity.default("day"),
|
|
64
|
+
/** Omit to count every event in the window. */
|
|
65
|
+
eventName: _zod.z.string().min(1).max(200).optional(),
|
|
66
|
+
breakdown: zTimeseriesBreakdown.optional(),
|
|
67
|
+
/**
|
|
68
|
+
* Cannot be combined with `breakdown` — the backend's breakdown time series
|
|
69
|
+
* takes no segment. Sending both is refused with `unsupported_combination`
|
|
70
|
+
* rather than silently ignoring one of them.
|
|
71
|
+
*/
|
|
72
|
+
segmentId: _zod.z.string().min(1).optional(),
|
|
73
|
+
/** Omit to fall back to the project's configured zone, then to UTC. */
|
|
74
|
+
timezone: zTimezone.optional()
|
|
75
|
+
});
|
|
76
|
+
var zTimeseriesPoint = _zod.z.object({
|
|
77
|
+
/** Bucket start, formatted by the analytics backend for the requested granularity. */
|
|
78
|
+
date: _zod.z.string(),
|
|
79
|
+
count: _zod.z.number().int().nonnegative()
|
|
80
|
+
});
|
|
81
|
+
var zEventsTimeseriesData = _zod.z.object({
|
|
82
|
+
granularity: zGranularity,
|
|
83
|
+
/** Zone the buckets were cut in. Mirrors `meta.timezone`. */
|
|
84
|
+
timezone: _zod.z.string(),
|
|
85
|
+
/** Total per bucket. Always present, with or without a breakdown. */
|
|
86
|
+
series: _zod.z.array(zTimeseriesPoint),
|
|
87
|
+
/** Present only when `breakdown` was requested: the top-N values, each with its own series. */
|
|
88
|
+
breakdown: _zod.z.array(
|
|
89
|
+
_zod.z.object({
|
|
90
|
+
value: _zod.z.string(),
|
|
91
|
+
points: _zod.z.array(zTimeseriesPoint)
|
|
92
|
+
})
|
|
93
|
+
).optional()
|
|
94
|
+
});
|
|
95
|
+
var zEventsTimeseriesResponse = zQueryEnvelope(zEventsTimeseriesData);
|
|
96
|
+
var zFunnelInput = _zod.z.object({
|
|
97
|
+
projectId: _zod.z.string().min(1),
|
|
98
|
+
/** Ordered event names. A user counts at step k if they reached at least step k. */
|
|
99
|
+
steps: _zod.z.array(_zod.z.string().min(1).max(200)).min(2).max(10),
|
|
100
|
+
startDate: _chunkZQVRSCTWcjs.zDate,
|
|
101
|
+
endDate: _chunkZQVRSCTWcjs.zDate,
|
|
102
|
+
/** Max seconds between the first and last step for a conversion to count. Default 24h. */
|
|
103
|
+
windowSeconds: _zod.z.number().int().min(60).max(2592e3).default(86400),
|
|
104
|
+
segmentId: _zod.z.string().min(1).optional(),
|
|
105
|
+
channelFilter: zChannelFilter,
|
|
106
|
+
/**
|
|
107
|
+
* Decides where `startDate` / `endDate` fall. A funnel has no buckets, so
|
|
108
|
+
* this only moves the window — but a nine-hour window shift changes who is
|
|
109
|
+
* in the funnel.
|
|
110
|
+
*/
|
|
111
|
+
timezone: zTimezone.optional()
|
|
112
|
+
});
|
|
113
|
+
var zFunnelStep = _zod.z.object({
|
|
114
|
+
step: _zod.z.number().int().min(1),
|
|
115
|
+
name: _zod.z.string(),
|
|
116
|
+
users: _zod.z.number().int().nonnegative(),
|
|
117
|
+
/** Percent of the previous step that reached this one. `100` for step 1. */
|
|
118
|
+
conversionRate: _zod.z.number(),
|
|
119
|
+
/** Percent of the previous step that did not. `0` for step 1. */
|
|
120
|
+
dropoffRate: _zod.z.number(),
|
|
121
|
+
dropoffUsers: _zod.z.number().int().nonnegative()
|
|
122
|
+
});
|
|
123
|
+
var zFunnelData = _zod.z.object({
|
|
124
|
+
steps: _zod.z.array(zFunnelStep),
|
|
125
|
+
totalUsers: _zod.z.number().int().nonnegative(),
|
|
126
|
+
/** Percent of step-1 users that reached the last step. */
|
|
127
|
+
overallConversionRate: _zod.z.number()
|
|
128
|
+
});
|
|
129
|
+
var zFunnelResponse = zQueryEnvelope(zFunnelData);
|
|
130
|
+
var zRetentionInterval = _zod.z.enum(["day", "week", "month"]);
|
|
131
|
+
var zRetentionInput = _zod.z.object({
|
|
132
|
+
projectId: _zod.z.string().min(1),
|
|
133
|
+
startDate: _chunkZQVRSCTWcjs.zDate,
|
|
134
|
+
endDate: _chunkZQVRSCTWcjs.zDate,
|
|
135
|
+
interval: zRetentionInterval.default("day"),
|
|
136
|
+
periods: _zod.z.number().int().min(1).max(30).default(7),
|
|
137
|
+
startEvent: _zod.z.string().min(1).max(200).optional(),
|
|
138
|
+
returnEvent: _zod.z.string().min(1).max(200).optional(),
|
|
139
|
+
/** `bounded`: returned in exactly period N. `unbounded`: returned in period N or later. */
|
|
140
|
+
retentionType: _zod.z.enum(["bounded", "unbounded"]).default("bounded"),
|
|
141
|
+
/**
|
|
142
|
+
* `first-ever` (default): the cohort is everyone whose true first activity
|
|
143
|
+
* landed in the window, read from the first-seen aggregate. `startEvent`,
|
|
144
|
+
* `segmentId` and `channelFilter` then constrain only the *returning* side.
|
|
145
|
+
* `first-in-window`: the cohort is selected by scanning events in the window
|
|
146
|
+
* with all filters applied. Slower, but filters apply to cohort selection.
|
|
147
|
+
*/
|
|
148
|
+
cohortMode: _zod.z.enum(["first-ever", "first-in-window"]).default("first-ever"),
|
|
149
|
+
segmentId: _zod.z.string().min(1).optional(),
|
|
150
|
+
channelFilter: zChannelFilter,
|
|
151
|
+
/**
|
|
152
|
+
* Decides where a cohort day starts, and so which period a return falls in.
|
|
153
|
+
*
|
|
154
|
+
* Honoured under both cohort modes. Note that a non-UTC zone makes
|
|
155
|
+
* `first-ever` read raw events instead of its daily rollup — the rollup is
|
|
156
|
+
* cut on UTC days and cannot be re-cut after the fact — so expect a slower
|
|
157
|
+
* query in exchange for cohorts that match the reader's calendar.
|
|
158
|
+
*/
|
|
159
|
+
timezone: zTimezone.optional()
|
|
160
|
+
});
|
|
161
|
+
var zRetentionCell = _zod.z.object({
|
|
162
|
+
/** `0` is the cohort's own period. */
|
|
163
|
+
period: _zod.z.number().int().nonnegative(),
|
|
164
|
+
users: _zod.z.number().int().nonnegative(),
|
|
165
|
+
/** Percent of `cohortSize`. */
|
|
166
|
+
rate: _zod.z.number()
|
|
167
|
+
});
|
|
168
|
+
var zRetentionData = _zod.z.object({
|
|
169
|
+
matrix: _zod.z.array(
|
|
170
|
+
_zod.z.object({
|
|
171
|
+
cohortDate: _zod.z.string(),
|
|
172
|
+
cohortSize: _zod.z.number().int().nonnegative(),
|
|
173
|
+
/** Length is `periods + 1`, index-aligned to `period`. */
|
|
174
|
+
retention: _zod.z.array(zRetentionCell)
|
|
175
|
+
})
|
|
176
|
+
),
|
|
177
|
+
/** Unweighted mean rate per period across cohorts. */
|
|
178
|
+
averageRetention: _zod.z.array(_zod.z.object({ period: _zod.z.number().int().nonnegative(), rate: _zod.z.number() })),
|
|
179
|
+
interval: zRetentionInterval,
|
|
180
|
+
periods: _zod.z.number().int(),
|
|
181
|
+
retentionType: _zod.z.enum(["bounded", "unbounded"]),
|
|
182
|
+
/** Zone the cohort days were cut in. Mirrors `meta.timezone`. */
|
|
183
|
+
timezone: _zod.z.string()
|
|
184
|
+
});
|
|
185
|
+
var zRetentionResponse = zQueryEnvelope(zRetentionData);
|
|
186
|
+
var QUERY_ERROR_CODES = [
|
|
187
|
+
/** 401 — unknown or mismatched credentials. */
|
|
188
|
+
"unauthorized",
|
|
189
|
+
/** 403 — the credential's project is not the project asked about. */
|
|
190
|
+
"project_scope_mismatch",
|
|
191
|
+
/** 403 — the credential lacks the `query` scope. Provision a query-scoped client. */
|
|
192
|
+
"query_scope_missing",
|
|
193
|
+
/** 400 — input failed the schema. `errors` carries the zod issues. */
|
|
194
|
+
"validation_failed",
|
|
195
|
+
/** 400 — inputs are individually valid but cannot be served together (e.g. breakdown + segmentId). */
|
|
196
|
+
"unsupported_combination",
|
|
197
|
+
/** 429 — per-credential query rate limit. `Retry-After` set. */
|
|
198
|
+
"rate_limited",
|
|
199
|
+
/** 504 — the analytics backend exceeded its statement timeout. Narrow the window. */
|
|
200
|
+
"query_timeout",
|
|
201
|
+
/** 500 — unexpected server failure. */
|
|
202
|
+
"internal_error"
|
|
203
|
+
];
|
|
204
|
+
var zQueryErrorCode = _zod.z.enum(QUERY_ERROR_CODES);
|
|
205
|
+
var zQueryError = _zod.z.object({
|
|
206
|
+
status: _zod.z.number().int(),
|
|
207
|
+
code: zQueryErrorCode,
|
|
208
|
+
error: _zod.z.string(),
|
|
209
|
+
message: _zod.z.string().optional(),
|
|
210
|
+
errors: _zod.z.unknown().optional(),
|
|
211
|
+
requestId: _zod.z.string().optional()
|
|
212
|
+
});
|
|
213
|
+
var QUERY_API_PATHS = {
|
|
214
|
+
eventsTimeseries: "/api/v1/query/events/timeseries",
|
|
215
|
+
funnel: "/api/v1/query/funnel",
|
|
216
|
+
retention: "/api/v1/query/retention"
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
exports.zGranularity = zGranularity; exports.zTimezone = zTimezone; exports.isValidTimeZone = isValidTimeZone; exports.zChannelFilter = zChannelFilter; exports.zQueryMeta = zQueryMeta; exports.zQueryEnvelope = zQueryEnvelope; exports.zTimeseriesBreakdown = zTimeseriesBreakdown; exports.zEventsTimeseriesInput = zEventsTimeseriesInput; exports.zTimeseriesPoint = zTimeseriesPoint; exports.zEventsTimeseriesData = zEventsTimeseriesData; exports.zEventsTimeseriesResponse = zEventsTimeseriesResponse; exports.zFunnelInput = zFunnelInput; exports.zFunnelStep = zFunnelStep; exports.zFunnelData = zFunnelData; exports.zFunnelResponse = zFunnelResponse; exports.zRetentionInterval = zRetentionInterval; exports.zRetentionInput = zRetentionInput; exports.zRetentionCell = zRetentionCell; exports.zRetentionData = zRetentionData; exports.zRetentionResponse = zRetentionResponse; exports.QUERY_ERROR_CODES = QUERY_ERROR_CODES; exports.zQueryErrorCode = zQueryErrorCode; exports.zQueryError = zQueryError; exports.QUERY_API_PATHS = QUERY_API_PATHS;
|
|
245
|
+
//# sourceMappingURL=chunk-DHSMXB54.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-DHSMXB54.cjs","../src/query.ts"],"names":[],"mappings":"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","file":"/Users/eric/reopt-ai/reopt-data/packages/data-contract/dist/chunk-DHSMXB54.cjs","sourcesContent":[null,"/**\n * `POST /api/v1/query/*` — the read contract.\n *\n * Server credentials, project-scoped: the authenticated client's projectId must\n * equal the `projectId` in the body, or the request is refused. Reads are POSTs\n * because the inputs nest (funnel steps, channel filters) and would not survive\n * a query string; consumers cache on their own side, so HTTP cache semantics\n * buy nothing here.\n *\n * Response shapes mirror the existing dashboard computations exactly — this\n * plane re-exposes them, it does not add new math.\n */\nimport { z } from \"zod\";\nimport { zDate, zInstant } from \"./index.js\";\n\nexport { zDate, zInstant };\n\n/** Bucket width for a time series. */\nexport const zGranularity = z.enum([\"hour\", \"day\", \"week\", \"month\"]);\nexport type Granularity = z.infer<typeof zGranularity>;\n\n/**\n * IANA time zone name, e.g. `Asia/Seoul`. Decides two things at once: where a\n * calendar day starts, and therefore which bucket an event falls in.\n *\n * A Korean brand reading \"23 Aug: 120 visits\" means 23 Aug in Seoul. Computed\n * in UTC, that headline silently covers 23 Aug 09:00 through 24 Aug 09:00 —\n * wrong by nine hours in a way nothing on the page reveals.\n *\n * Validated against the runtime's own zone database rather than a regex, so an\n * unknown name fails here with a clear message instead of deep inside a query.\n */\nexport const zTimezone = z.string().min(1).max(64).refine(isValidTimeZone, { message: \"unknown IANA time zone\" });\n\nexport function isValidTimeZone(value: string): boolean {\n try {\n new Intl.DateTimeFormat(\"en-US\", { timeZone: value });\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Acquisition-channel filter. Every field is a set — an event matches when its\n * value is in the set; omitted fields do not constrain. Mirrors the dashboard's\n * `channelFilterSchema` field-for-field.\n */\nexport const zChannelFilter = z\n .object({\n referrerTypes: z.array(z.string()).optional(),\n channelTypes: z.array(z.string()).optional(),\n utmSources: z.array(z.string()).optional(),\n utmMediums: z.array(z.string()).optional(),\n utmCampaigns: z.array(z.string()).optional(),\n })\n .optional();\nexport type ChannelFilter = z.infer<typeof zChannelFilter>;\n\n// ─── Envelope ───────────────────────────────────────────────────────────────\n\n/**\n * Staleness is reported as two independent numbers, never as one sum, because\n * the two have different remedies: a stale cache resolves itself at the next\n * TTL, while ingest lag means the read model is genuinely behind and the right\n * response is an \"aggregating\" badge, not a refresh.\n */\nexport const zQueryMeta = z.object({\n projectId: z.string(),\n /** When the underlying computation actually ran. On a cache hit, when the cached value was computed. */\n computedAt: zInstant,\n /** Age of the returned computation, in seconds. `0` on a cache miss. */\n cacheAgeSeconds: z.number().int().nonnegative(),\n /**\n * How far behind materialization was when the computation ran, in seconds:\n * the age of the oldest raw event not yet materialized, or `0` when nothing\n * is pending.\n *\n * Deliberately not `now - max(events.created_at)` — that conflates \"the\n * pipeline is behind\" with \"this project is quiet\", and grows without bound\n * on an idle project.\n */\n ingestLagSeconds: z.number().int().nonnegative(),\n cached: z.boolean(),\n /**\n * The time zone actually used to place bucket and window boundaries — the\n * resolved value, not the requested one. Read it rather than assuming the\n * request was honoured: when no zone is given anywhere it is `\"UTC\"`, and\n * that is exactly the case a consumer needs to notice.\n */\n timezone: z.string(),\n requestId: z.string(),\n});\nexport type QueryMeta = z.infer<typeof zQueryMeta>;\n\n/** Every query response is `{ data, meta }`. */\nexport function zQueryEnvelope<T extends z.ZodTypeAny>(data: T) {\n return z.object({ data, meta: zQueryMeta });\n}\n\n// ─── events.timeseries ──────────────────────────────────────────────────────\n\n/**\n * v1 breaks down by `properties` JSON keys only. Built-in columns\n * (country / os / browser / utm_*) have a totals breakdown in the backend but\n * no *time series* variant, and this plane does not add new computations.\n */\nexport const zTimeseriesBreakdown = z.object({\n kind: z.literal(\"property\"),\n key: z.string().min(1).max(200),\n topN: z.number().int().min(1).max(10).default(5),\n});\nexport type TimeseriesBreakdown = z.infer<typeof zTimeseriesBreakdown>;\n\nexport const zEventsTimeseriesInput = z.object({\n projectId: z.string().min(1),\n startDate: zDate,\n endDate: zDate,\n granularity: zGranularity.default(\"day\"),\n /** Omit to count every event in the window. */\n eventName: z.string().min(1).max(200).optional(),\n breakdown: zTimeseriesBreakdown.optional(),\n /**\n * Cannot be combined with `breakdown` — the backend's breakdown time series\n * takes no segment. Sending both is refused with `unsupported_combination`\n * rather than silently ignoring one of them.\n */\n segmentId: z.string().min(1).optional(),\n /** Omit to fall back to the project's configured zone, then to UTC. */\n timezone: zTimezone.optional(),\n});\nexport type EventsTimeseriesInput = z.input<typeof zEventsTimeseriesInput>;\n\nexport const zTimeseriesPoint = z.object({\n /** Bucket start, formatted by the analytics backend for the requested granularity. */\n date: z.string(),\n count: z.number().int().nonnegative(),\n});\nexport type TimeseriesPoint = z.infer<typeof zTimeseriesPoint>;\n\nexport const zEventsTimeseriesData = z.object({\n granularity: zGranularity,\n /** Zone the buckets were cut in. Mirrors `meta.timezone`. */\n timezone: z.string(),\n /** Total per bucket. Always present, with or without a breakdown. */\n series: z.array(zTimeseriesPoint),\n /** Present only when `breakdown` was requested: the top-N values, each with its own series. */\n breakdown: z\n .array(\n z.object({\n value: z.string(),\n points: z.array(zTimeseriesPoint),\n })\n )\n .optional(),\n});\nexport type EventsTimeseriesData = z.infer<typeof zEventsTimeseriesData>;\n\nexport const zEventsTimeseriesResponse = zQueryEnvelope(zEventsTimeseriesData);\nexport type EventsTimeseriesResponse = z.infer<typeof zEventsTimeseriesResponse>;\n\n// ─── funnel ─────────────────────────────────────────────────────────────────\n\nexport const zFunnelInput = z.object({\n projectId: z.string().min(1),\n /** Ordered event names. A user counts at step k if they reached at least step k. */\n steps: z.array(z.string().min(1).max(200)).min(2).max(10),\n startDate: zDate,\n endDate: zDate,\n /** Max seconds between the first and last step for a conversion to count. Default 24h. */\n windowSeconds: z.number().int().min(60).max(2_592_000).default(86_400),\n segmentId: z.string().min(1).optional(),\n channelFilter: zChannelFilter,\n /**\n * Decides where `startDate` / `endDate` fall. A funnel has no buckets, so\n * this only moves the window — but a nine-hour window shift changes who is\n * in the funnel.\n */\n timezone: zTimezone.optional(),\n});\nexport type FunnelInput = z.input<typeof zFunnelInput>;\n\nexport const zFunnelStep = z.object({\n step: z.number().int().min(1),\n name: z.string(),\n users: z.number().int().nonnegative(),\n /** Percent of the previous step that reached this one. `100` for step 1. */\n conversionRate: z.number(),\n /** Percent of the previous step that did not. `0` for step 1. */\n dropoffRate: z.number(),\n dropoffUsers: z.number().int().nonnegative(),\n});\n\nexport const zFunnelData = z.object({\n steps: z.array(zFunnelStep),\n totalUsers: z.number().int().nonnegative(),\n /** Percent of step-1 users that reached the last step. */\n overallConversionRate: z.number(),\n});\nexport type FunnelData = z.infer<typeof zFunnelData>;\n\nexport const zFunnelResponse = zQueryEnvelope(zFunnelData);\nexport type FunnelResponse = z.infer<typeof zFunnelResponse>;\n\n// ─── retention ──────────────────────────────────────────────────────────────\n\nexport const zRetentionInterval = z.enum([\"day\", \"week\", \"month\"]);\n\nexport const zRetentionInput = z.object({\n projectId: z.string().min(1),\n startDate: zDate,\n endDate: zDate,\n interval: zRetentionInterval.default(\"day\"),\n periods: z.number().int().min(1).max(30).default(7),\n startEvent: z.string().min(1).max(200).optional(),\n returnEvent: z.string().min(1).max(200).optional(),\n /** `bounded`: returned in exactly period N. `unbounded`: returned in period N or later. */\n retentionType: z.enum([\"bounded\", \"unbounded\"]).default(\"bounded\"),\n /**\n * `first-ever` (default): the cohort is everyone whose true first activity\n * landed in the window, read from the first-seen aggregate. `startEvent`,\n * `segmentId` and `channelFilter` then constrain only the *returning* side.\n * `first-in-window`: the cohort is selected by scanning events in the window\n * with all filters applied. Slower, but filters apply to cohort selection.\n */\n cohortMode: z.enum([\"first-ever\", \"first-in-window\"]).default(\"first-ever\"),\n segmentId: z.string().min(1).optional(),\n channelFilter: zChannelFilter,\n /**\n * Decides where a cohort day starts, and so which period a return falls in.\n *\n * Honoured under both cohort modes. Note that a non-UTC zone makes\n * `first-ever` read raw events instead of its daily rollup — the rollup is\n * cut on UTC days and cannot be re-cut after the fact — so expect a slower\n * query in exchange for cohorts that match the reader's calendar.\n */\n timezone: zTimezone.optional(),\n});\nexport type RetentionInput = z.input<typeof zRetentionInput>;\n\nexport const zRetentionCell = z.object({\n /** `0` is the cohort's own period. */\n period: z.number().int().nonnegative(),\n users: z.number().int().nonnegative(),\n /** Percent of `cohortSize`. */\n rate: z.number(),\n});\n\nexport const zRetentionData = z.object({\n matrix: z.array(\n z.object({\n cohortDate: z.string(),\n cohortSize: z.number().int().nonnegative(),\n /** Length is `periods + 1`, index-aligned to `period`. */\n retention: z.array(zRetentionCell),\n })\n ),\n /** Unweighted mean rate per period across cohorts. */\n averageRetention: z.array(z.object({ period: z.number().int().nonnegative(), rate: z.number() })),\n interval: zRetentionInterval,\n periods: z.number().int(),\n retentionType: z.enum([\"bounded\", \"unbounded\"]),\n /** Zone the cohort days were cut in. Mirrors `meta.timezone`. */\n timezone: z.string(),\n});\nexport type RetentionData = z.infer<typeof zRetentionData>;\n\nexport const zRetentionResponse = zQueryEnvelope(zRetentionData);\nexport type RetentionResponse = z.infer<typeof zRetentionResponse>;\n\n// ─── Errors ─────────────────────────────────────────────────────────────────\n\nexport const QUERY_ERROR_CODES = [\n /** 401 — unknown or mismatched credentials. */\n \"unauthorized\",\n /** 403 — the credential's project is not the project asked about. */\n \"project_scope_mismatch\",\n /** 403 — the credential lacks the `query` scope. Provision a query-scoped client. */\n \"query_scope_missing\",\n /** 400 — input failed the schema. `errors` carries the zod issues. */\n \"validation_failed\",\n /** 400 — inputs are individually valid but cannot be served together (e.g. breakdown + segmentId). */\n \"unsupported_combination\",\n /** 429 — per-credential query rate limit. `Retry-After` set. */\n \"rate_limited\",\n /** 504 — the analytics backend exceeded its statement timeout. Narrow the window. */\n \"query_timeout\",\n /** 500 — unexpected server failure. */\n \"internal_error\",\n] as const;\nexport const zQueryErrorCode = z.enum(QUERY_ERROR_CODES);\nexport type QueryErrorCode = z.infer<typeof zQueryErrorCode>;\n\nexport const zQueryError = z.object({\n status: z.number().int(),\n code: zQueryErrorCode,\n error: z.string(),\n message: z.string().optional(),\n errors: z.unknown().optional(),\n requestId: z.string().optional(),\n});\nexport type QueryError = z.infer<typeof zQueryError>;\n\n/** Paths, so the client and the route handlers cannot drift apart. */\nexport const QUERY_API_PATHS = {\n eventsTimeseries: \"/api/v1/query/events/timeseries\",\n funnel: \"/api/v1/query/funnel\",\n retention: \"/api/v1/query/retention\",\n} as const;\n"]}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// src/ingest.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var RESERVED_EVENT_NAMES = ["session_start", "session_end", "screen_view"];
|
|
4
|
+
var MAX_TRACK_PAYLOAD_BYTES = 512e3;
|
|
5
|
+
var zClientEventMetadata = {
|
|
6
|
+
/**
|
|
7
|
+
* Client-generated UUID. This is the idempotency key: it becomes the
|
|
8
|
+
* `RawEvent` primary key, scoped to the project. Re-sending the same
|
|
9
|
+
* eventId is always safe and always reported as a duplicate.
|
|
10
|
+
*/
|
|
11
|
+
eventId: z.uuid(),
|
|
12
|
+
/** Event time, epoch milliseconds. */
|
|
13
|
+
timestamp: z.number().int().nonnegative()
|
|
14
|
+
};
|
|
15
|
+
var zTrackPayload = z.object({
|
|
16
|
+
name: z.string().min(1).max(200),
|
|
17
|
+
properties: z.record(z.string(), z.unknown()).optional(),
|
|
18
|
+
profileId: z.string().max(500).or(z.number()).optional()
|
|
19
|
+
}).refine((data) => !RESERVED_EVENT_NAMES.includes(data.name), {
|
|
20
|
+
message: `Event name cannot be one of the reserved names: ${RESERVED_EVENT_NAMES.join(", ")}`,
|
|
21
|
+
path: ["name"]
|
|
22
|
+
});
|
|
23
|
+
var zIdentifyPayload = z.object({
|
|
24
|
+
profileId: z.string().min(1).max(500).or(z.number()),
|
|
25
|
+
firstName: z.string().max(200).optional(),
|
|
26
|
+
lastName: z.string().max(200).optional(),
|
|
27
|
+
email: z.email().max(320).optional(),
|
|
28
|
+
avatar: z.url().max(2e3).optional(),
|
|
29
|
+
properties: z.record(z.string(), z.unknown()).optional()
|
|
30
|
+
});
|
|
31
|
+
var zIncrementPayload = z.object({
|
|
32
|
+
profileId: z.string().min(1).max(500).or(z.number()),
|
|
33
|
+
property: z.string().min(1).max(200),
|
|
34
|
+
value: z.number().positive().optional()
|
|
35
|
+
});
|
|
36
|
+
var zDecrementPayload = z.object({
|
|
37
|
+
profileId: z.string().min(1).max(500).or(z.number()),
|
|
38
|
+
property: z.string().min(1).max(200),
|
|
39
|
+
value: z.number().positive().optional()
|
|
40
|
+
});
|
|
41
|
+
var zTrackHandlerPayload = z.discriminatedUnion("type", [
|
|
42
|
+
z.object({ type: z.literal("track"), payload: zTrackPayload, ...zClientEventMetadata }),
|
|
43
|
+
z.object({ type: z.literal("identify"), payload: zIdentifyPayload, ...zClientEventMetadata }),
|
|
44
|
+
z.object({ type: z.literal("increment"), payload: zIncrementPayload, ...zClientEventMetadata }),
|
|
45
|
+
z.object({ type: z.literal("decrement"), payload: zDecrementPayload, ...zClientEventMetadata })
|
|
46
|
+
]);
|
|
47
|
+
var INGEST_MODES = ["browser", "server"];
|
|
48
|
+
var zIngestMode = z.enum(INGEST_MODES);
|
|
49
|
+
var INGEST_REJECTION_REASONS = [
|
|
50
|
+
/** Failed the zod schema (bad eventId, missing name, wrong types…). */
|
|
51
|
+
"validation_failed",
|
|
52
|
+
/** Used one of RESERVED_EVENT_NAMES. */
|
|
53
|
+
"reserved_name",
|
|
54
|
+
/** Another row earlier in the same batch already claimed this eventId. */
|
|
55
|
+
"duplicate_in_batch"
|
|
56
|
+
];
|
|
57
|
+
var zIngestRejectionReason = z.enum(INGEST_REJECTION_REASONS);
|
|
58
|
+
var zIngestRejection = z.object({
|
|
59
|
+
/** May be absent or malformed on `validation_failed` — hence `string`, not `uuid`. */
|
|
60
|
+
eventId: z.string(),
|
|
61
|
+
/** Position in the submitted batch. The only reliable identifier when eventId itself is bad. */
|
|
62
|
+
index: z.number().int().nonnegative(),
|
|
63
|
+
reason: zIngestRejectionReason,
|
|
64
|
+
message: z.string().optional()
|
|
65
|
+
});
|
|
66
|
+
var ingestCounts = {
|
|
67
|
+
/** Raw events newly persisted by this request. */
|
|
68
|
+
accepted: z.number().int().nonnegative(),
|
|
69
|
+
/**
|
|
70
|
+
* Events the server already had. In `server` mode this is exactly the
|
|
71
|
+
* eventId-idempotency count; in `browser` mode it also includes the
|
|
72
|
+
* 5-second click dedup.
|
|
73
|
+
*/
|
|
74
|
+
duplicates: z.number().int().nonnegative(),
|
|
75
|
+
/** Always `[]` in `browser` mode, where a bad row fails the whole batch. */
|
|
76
|
+
rejected: z.array(zIngestRejection)
|
|
77
|
+
};
|
|
78
|
+
var zIngestOkResponse = z.object({
|
|
79
|
+
status: z.literal("ok"),
|
|
80
|
+
requestId: z.string(),
|
|
81
|
+
mode: zIngestMode,
|
|
82
|
+
...ingestCounts,
|
|
83
|
+
queued: z.boolean()
|
|
84
|
+
});
|
|
85
|
+
var zIngestAcceptedResponse = z.object({
|
|
86
|
+
status: z.literal("accepted"),
|
|
87
|
+
requestId: z.string(),
|
|
88
|
+
mode: zIngestMode,
|
|
89
|
+
...ingestCounts,
|
|
90
|
+
backgroundQueued: z.literal(false),
|
|
91
|
+
replay: z.literal("unmaterialized-raw-events")
|
|
92
|
+
});
|
|
93
|
+
var zIngestResponse = z.discriminatedUnion("status", [zIngestOkResponse, zIngestAcceptedResponse]);
|
|
94
|
+
function reconcileIngestResponse(response, sentCount) {
|
|
95
|
+
return response.accepted + response.duplicates + response.rejected.length === sentCount;
|
|
96
|
+
}
|
|
97
|
+
var INGEST_ERROR_CODES = [
|
|
98
|
+
/** 401 — unknown or mismatched credentials. Stop and alert; do not retry. */
|
|
99
|
+
"unauthorized",
|
|
100
|
+
/** 400 — body was not valid JSON. */
|
|
101
|
+
"invalid_json",
|
|
102
|
+
/** 400 — request exceeded MAX_TRACK_PAYLOAD_BYTES. Split the batch. */
|
|
103
|
+
"payload_too_large",
|
|
104
|
+
/** 400 — zero events submitted. */
|
|
105
|
+
"empty_batch",
|
|
106
|
+
/** 400 — batch contained a `type: "alias"` event, which is not supported. */
|
|
107
|
+
"alias_unsupported",
|
|
108
|
+
/** 400 — the project has no organization; it cannot be billed or quota-checked. */
|
|
109
|
+
"project_without_organization",
|
|
110
|
+
/** 400 — `browser` mode only: a row failed validation. `server` mode reports these in `rejected[]`. */
|
|
111
|
+
"validation_failed",
|
|
112
|
+
/** 429 — per-project request rate limit. `Retry-After` set. Pause this tick, keep the cursor. */
|
|
113
|
+
"rate_limited",
|
|
114
|
+
/** 429 — the organization's monthly event quota is exhausted. `Retry-After` set (capped at 1h). Alert: a human must raise the limit. */
|
|
115
|
+
"quota_exceeded",
|
|
116
|
+
/** 500 — unexpected server failure. Back off and retry. */
|
|
117
|
+
"internal_error"
|
|
118
|
+
];
|
|
119
|
+
var zIngestErrorCode = z.enum(INGEST_ERROR_CODES);
|
|
120
|
+
var zIngestError = z.object({
|
|
121
|
+
status: z.number().int(),
|
|
122
|
+
code: zIngestErrorCode,
|
|
123
|
+
error: z.string(),
|
|
124
|
+
message: z.string().optional(),
|
|
125
|
+
/** zod issues, when `code === "validation_failed"`. */
|
|
126
|
+
errors: z.unknown().optional(),
|
|
127
|
+
requestId: z.string().optional()
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
RESERVED_EVENT_NAMES,
|
|
132
|
+
MAX_TRACK_PAYLOAD_BYTES,
|
|
133
|
+
zTrackPayload,
|
|
134
|
+
zIdentifyPayload,
|
|
135
|
+
zIncrementPayload,
|
|
136
|
+
zDecrementPayload,
|
|
137
|
+
zTrackHandlerPayload,
|
|
138
|
+
INGEST_MODES,
|
|
139
|
+
zIngestMode,
|
|
140
|
+
INGEST_REJECTION_REASONS,
|
|
141
|
+
zIngestRejectionReason,
|
|
142
|
+
zIngestRejection,
|
|
143
|
+
zIngestOkResponse,
|
|
144
|
+
zIngestAcceptedResponse,
|
|
145
|
+
zIngestResponse,
|
|
146
|
+
reconcileIngestResponse,
|
|
147
|
+
INGEST_ERROR_CODES,
|
|
148
|
+
zIngestErrorCode,
|
|
149
|
+
zIngestError
|
|
150
|
+
};
|
|
151
|
+
//# sourceMappingURL=chunk-FRJK7OCQ.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\";\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/** 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 idempotency key: it becomes the\n * `RawEvent` primary key, scoped to the project. Re-sending the same\n * eventId is 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\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 /** 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":[]}
|