stitchkit 0.80.1 → 0.81.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/dist/agent-runtime/provider-failure.d.ts.map +1 -1
- package/dist/agent-runtime-coding-tools.js +1 -1
- package/dist/agent-runtime-harness.js +7 -5
- package/dist/agent-runtime.js +15 -19
- package/dist/application/notification-outbox.d.ts +80 -0
- package/dist/application/notification-outbox.d.ts.map +1 -0
- package/dist/application/process-lifecycle.d.ts +166 -0
- package/dist/application/process-lifecycle.d.ts.map +1 -0
- package/dist/application/state-store.d.ts +18 -0
- package/dist/application/state-store.d.ts.map +1 -0
- package/dist/application.d.ts +3 -0
- package/dist/application.d.ts.map +1 -1
- package/dist/application.js +568 -11
- package/dist/browser/http.d.ts +14 -0
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/browser/resumable.d.ts +6 -0
- package/dist/browser/resumable.d.ts.map +1 -1
- package/dist/cli.js +9 -7
- package/dist/contract/define.d.ts +27 -4
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/errors-factory.d.ts +22 -3
- package/dist/contract/errors-factory.d.ts.map +1 -1
- package/dist/contract/index.d.ts +2 -2
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/geo/index.d.ts +4 -0
- package/dist/geo/index.d.ts.map +1 -0
- package/dist/geo/maxmind.d.ts +4 -0
- package/dist/geo/maxmind.d.ts.map +1 -0
- package/dist/geo/resolver.d.ts +9 -0
- package/dist/geo/resolver.d.ts.map +1 -0
- package/dist/geo/types.d.ts +59 -0
- package/dist/geo/types.d.ts.map +1 -0
- package/dist/geo.d.ts +3 -0
- package/dist/geo.d.ts.map +1 -0
- package/dist/geo.js +245 -0
- package/dist/{index-9s6z8vgf.js → index-1dd82z59.js} +1 -1
- package/dist/{index-vwp69871.js → index-27487ay5.js} +4 -4
- package/dist/{index-7ag81v2f.js → index-27gm3d6g.js} +10 -321
- package/dist/{index-y01f4223.js → index-2bxcmqkb.js} +127 -43
- package/dist/{index-dxesg10d.js → index-3ds42w1w.js} +3 -3
- package/dist/index-3xwxfj4z.js +334 -0
- package/dist/{index-egaabxkz.js → index-5z8tezpx.js} +2 -2
- package/dist/{index-fxz0v7vg.js → index-7evdsyez.js} +1 -83
- package/dist/{index-xcetq6j3.js → index-90d7h2cr.js} +32 -13
- package/dist/{index-t0z5p5w6.js → index-apn8repm.js} +1 -1
- package/dist/{index-758zx2ha.js → index-dggwnnjv.js} +2 -2
- package/dist/{index-dwf1f2ap.js → index-f9mb610r.js} +1 -62
- package/dist/{index-5mtc969c.js → index-fwbkp2jn.js} +1 -1
- package/dist/index-p13mwz16.js +16 -0
- package/dist/{index-m9kzax52.js → index-p18f59zy.js} +21 -5
- package/dist/{index-hb5pf3s4.js → index-qmgs7y68.js} +1 -1
- package/dist/{index-2rwwmr7v.js → index-s4ckmrac.js} +1 -1
- package/dist/{index-h4pj6fta.js → index-s8cnx02s.js} +8 -5
- package/dist/{index-k9ay6v6d.js → index-vchzar7e.js} +74 -23
- package/dist/index-vsbzgd7b.js +84 -0
- package/dist/{index-dxjjv399.js → index-xbppf54a.js} +95 -15
- package/dist/index-y91ry9h2.js +4 -0
- package/dist/index-zcgf3gqf.js +83 -0
- package/dist/index.js +17 -13
- package/dist/internal/error-message.d.ts +3 -0
- package/dist/internal/error-message.d.ts.map +1 -0
- package/dist/internal/media-type.d.ts +8 -0
- package/dist/internal/media-type.d.ts.map +1 -0
- package/dist/internal/route-pattern.d.ts +21 -0
- package/dist/internal/route-pattern.d.ts.map +1 -1
- package/dist/node.js +9 -6
- package/dist/observability/audit.d.ts +28 -1
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/bounded-logger.d.ts +20 -0
- package/dist/observability/bounded-logger.d.ts.map +1 -0
- package/dist/observability/context.d.ts +12 -3
- package/dist/observability/context.d.ts.map +1 -1
- package/dist/observability/index.d.ts +3 -2
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +138 -8
- package/dist/observability/sanitize.d.ts +17 -0
- package/dist/observability/sanitize.d.ts.map +1 -1
- package/dist/primitives.js +1 -1
- package/dist/react/query-client.d.ts +33 -0
- package/dist/react/query-client.d.ts.map +1 -0
- package/dist/react.d.ts +1 -0
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +83 -1
- package/dist/realtime/registry.d.ts +93 -0
- package/dist/realtime/registry.d.ts.map +1 -0
- package/dist/release/header.d.ts +9 -0
- package/dist/release/header.d.ts.map +1 -0
- package/dist/release/marker.d.ts +39 -0
- package/dist/release/marker.d.ts.map +1 -0
- package/dist/release/socket.d.ts +44 -0
- package/dist/release/socket.d.ts.map +1 -0
- package/dist/release/watcher.d.ts +66 -0
- package/dist/release/watcher.d.ts.map +1 -0
- package/dist/release.d.ts +15 -0
- package/dist/release.d.ts.map +1 -0
- package/dist/release.js +196 -0
- package/dist/remote.js +12 -7
- package/dist/server/context.d.ts +2 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/error-hook.d.ts +17 -16
- package/dist/server/error-hook.d.ts.map +1 -1
- package/dist/server/file-state-store.d.ts +23 -0
- package/dist/server/file-state-store.d.ts.map +1 -0
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +5 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +469 -11
- package/dist/server/middleware/cookies.d.ts +33 -1
- package/dist/server/middleware/cookies.d.ts.map +1 -1
- package/dist/server/middleware/cors.d.ts +16 -1
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/release-signal.d.ts +35 -0
- package/dist/server/release-signal.d.ts.map +1 -0
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/types.d.ts +26 -2
- package/dist/server/types.d.ts.map +1 -1
- package/dist/testing/surface-manifest.d.ts.map +1 -1
- package/dist/testing.js +7 -4
- package/dist/tool-invoker.js +8 -6
- package/dist/tools/download-core.d.ts.map +1 -1
- package/dist/tools/remote.d.ts.map +1 -1
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools.js +20 -16
- package/dist/tracking/attribution.d.ts +51 -0
- package/dist/tracking/attribution.d.ts.map +1 -0
- package/dist/tracking/beacon.d.ts +14 -0
- package/dist/tracking/beacon.d.ts.map +1 -0
- package/dist/tracking/clicks.d.ts +46 -0
- package/dist/tracking/clicks.d.ts.map +1 -0
- package/dist/tracking/client.d.ts +121 -0
- package/dist/tracking/client.d.ts.map +1 -0
- package/dist/tracking/contract.d.ts +46 -0
- package/dist/tracking/contract.d.ts.map +1 -0
- package/dist/tracking/delivery.d.ts +24 -0
- package/dist/tracking/delivery.d.ts.map +1 -0
- package/dist/tracking/host.d.ts +43 -0
- package/dist/tracking/host.d.ts.map +1 -0
- package/dist/tracking/once-per-page.d.ts +17 -0
- package/dist/tracking/once-per-page.d.ts.map +1 -0
- package/dist/tracking/outbox-storage-indexeddb.d.ts +12 -0
- package/dist/tracking/outbox-storage-indexeddb.d.ts.map +1 -0
- package/dist/tracking/outbox-storage-memory.d.ts +12 -0
- package/dist/tracking/outbox-storage-memory.d.ts.map +1 -0
- package/dist/tracking/outbox.d.ts +98 -0
- package/dist/tracking/outbox.d.ts.map +1 -0
- package/dist/tracking/schemas.d.ts +183 -0
- package/dist/tracking/schemas.d.ts.map +1 -0
- package/dist/tracking/scroll.d.ts +22 -0
- package/dist/tracking/scroll.d.ts.map +1 -0
- package/dist/tracking/sequence-reserve.d.ts +34 -0
- package/dist/tracking/sequence-reserve.d.ts.map +1 -0
- package/dist/tracking/server/active-interval.d.ts +26 -0
- package/dist/tracking/server/active-interval.d.ts.map +1 -0
- package/dist/tracking/server/bot.d.ts +9 -0
- package/dist/tracking/server/bot.d.ts.map +1 -0
- package/dist/tracking/server/disposition.d.ts +55 -0
- package/dist/tracking/server/disposition.d.ts.map +1 -0
- package/dist/tracking/server/hash.d.ts +13 -0
- package/dist/tracking/server/hash.d.ts.map +1 -0
- package/dist/tracking/server/presence.d.ts +27 -0
- package/dist/tracking/server/presence.d.ts.map +1 -0
- package/dist/tracking/server/visit-lease.d.ts +92 -0
- package/dist/tracking/server/visit-lease.d.ts.map +1 -0
- package/dist/tracking/visible-time.d.ts +38 -0
- package/dist/tracking/visible-time.d.ts.map +1 -0
- package/dist/tracking-server.d.ts +14 -0
- package/dist/tracking-server.d.ts.map +1 -0
- package/dist/tracking-server.js +173 -0
- package/dist/tracking.d.ts +24 -0
- package/dist/tracking.d.ts.map +1 -0
- package/dist/tracking.js +1090 -0
- package/llms-full.txt +938 -29
- package/llms.txt +4 -0
- package/package.json +25 -4
package/dist/tracking.js
ADDED
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApiError
|
|
3
|
+
} from "./index-3xwxfj4z.js";
|
|
4
|
+
import {
|
|
5
|
+
defineContract
|
|
6
|
+
} from "./index-2bxcmqkb.js";
|
|
7
|
+
import"./index-6k1937bx.js";
|
|
8
|
+
import"./index-0w9abg87.js";
|
|
9
|
+
import"./index-zcgf3gqf.js";
|
|
10
|
+
import"./index-kzfs85xp.js";
|
|
11
|
+
import"./index-77fekveh.js";
|
|
12
|
+
|
|
13
|
+
// src/tracking/attribution.ts
|
|
14
|
+
import { z as z2 } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/tracking/schemas.ts
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
var UtmDataSchema = z.object({
|
|
19
|
+
source: z.string().max(200).optional(),
|
|
20
|
+
medium: z.string().max(200).optional(),
|
|
21
|
+
campaign: z.string().max(200).optional(),
|
|
22
|
+
content: z.string().max(200).optional(),
|
|
23
|
+
term: z.string().max(200).optional()
|
|
24
|
+
});
|
|
25
|
+
var AttributionDataSchema = z.object({
|
|
26
|
+
utm: UtmDataSchema.optional(),
|
|
27
|
+
referrer: z.string().optional(),
|
|
28
|
+
landingPage: z.string().optional()
|
|
29
|
+
});
|
|
30
|
+
var DEFAULT_BUILD_ID_PATTERN = /^(dev|[a-f0-9]{7,40})$/;
|
|
31
|
+
var TrackingDispositionStatusSchema = z.enum([
|
|
32
|
+
"accepted",
|
|
33
|
+
"duplicate",
|
|
34
|
+
"identity-invalid",
|
|
35
|
+
"excluded-bot"
|
|
36
|
+
]);
|
|
37
|
+
var TrackingDispositionSchema = z.object({
|
|
38
|
+
eventId: z.uuid(),
|
|
39
|
+
status: TrackingDispositionStatusSchema
|
|
40
|
+
});
|
|
41
|
+
var TrackEventsResponseSchema = z.object({
|
|
42
|
+
accepted: z.number().int().nonnegative(),
|
|
43
|
+
dispositions: z.array(TrackingDispositionSchema)
|
|
44
|
+
});
|
|
45
|
+
var VisitBootstrapResponseSchema = z.object({
|
|
46
|
+
visitId: z.uuid(),
|
|
47
|
+
expiresAt: z.iso.datetime()
|
|
48
|
+
});
|
|
49
|
+
var TrackingOutboxStateSchema = z.enum(["available", "unavailable"]);
|
|
50
|
+
function trackingEventShape(eventTypes) {
|
|
51
|
+
return {
|
|
52
|
+
eventId: z.uuid(),
|
|
53
|
+
visitId: z.uuid(),
|
|
54
|
+
browserStreamId: z.uuid(),
|
|
55
|
+
browserSequence: z.number().int().nonnegative(),
|
|
56
|
+
type: z.enum(eventTypes),
|
|
57
|
+
page: z.string().max(2000),
|
|
58
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
59
|
+
clientTimestamp: z.number()
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function visitEntryContextShape(buildIdPattern) {
|
|
63
|
+
return {
|
|
64
|
+
browserStreamId: z.uuid(),
|
|
65
|
+
previousVisitId: z.uuid().optional(),
|
|
66
|
+
origin: z.url().max(500),
|
|
67
|
+
landingPath: z.string().startsWith("/").max(300).regex(/^[^?#]*$/),
|
|
68
|
+
referrer: z.url().max(2000).optional(),
|
|
69
|
+
utm: UtmDataSchema.optional(),
|
|
70
|
+
displayMode: z.enum(["browser", "standalone"]),
|
|
71
|
+
screenWidth: z.number().int().positive().max(20000),
|
|
72
|
+
screenHeight: z.number().int().positive().max(20000),
|
|
73
|
+
buildId: z.string().regex(buildIdPattern),
|
|
74
|
+
outboxState: TrackingOutboxStateSchema,
|
|
75
|
+
outboxQueued: z.number().int().nonnegative().max(1000),
|
|
76
|
+
outboxDropped: z.number().int().nonnegative()
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function createTrackingSchemas(config) {
|
|
80
|
+
const base = z.object(trackingEventShape(config.eventTypes));
|
|
81
|
+
const event = config.eventExtras ? base.extend(config.eventExtras.shape) : base;
|
|
82
|
+
const buildIdPattern = config.buildIdPattern ?? DEFAULT_BUILD_ID_PATTERN;
|
|
83
|
+
const maxEvents = config.maxEventsPerBatch ?? 50;
|
|
84
|
+
return {
|
|
85
|
+
event,
|
|
86
|
+
request: z.object({
|
|
87
|
+
buildId: z.string().regex(buildIdPattern),
|
|
88
|
+
events: z.array(event).min(1).max(maxEvents),
|
|
89
|
+
utm: UtmDataSchema.optional()
|
|
90
|
+
}),
|
|
91
|
+
response: TrackEventsResponseSchema,
|
|
92
|
+
disposition: TrackingDispositionSchema,
|
|
93
|
+
entry: z.object(visitEntryContextShape(buildIdPattern)),
|
|
94
|
+
bootstrap: VisitBootstrapResponseSchema
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/tracking/attribution.ts
|
|
99
|
+
var StoredTouchSchema = z2.object({ data: AttributionDataSchema, capturedAt: z2.number() });
|
|
100
|
+
var StoredAttributionSchema = z2.object({
|
|
101
|
+
firstTouch: StoredTouchSchema,
|
|
102
|
+
currentTouch: StoredTouchSchema
|
|
103
|
+
});
|
|
104
|
+
function normalize(value) {
|
|
105
|
+
if (!value)
|
|
106
|
+
return;
|
|
107
|
+
return value.toLowerCase().trim().slice(0, 200) || undefined;
|
|
108
|
+
}
|
|
109
|
+
function parseUtmFromSearch(search) {
|
|
110
|
+
const params = new URLSearchParams(search);
|
|
111
|
+
const source = normalize(params.get("utm_source"));
|
|
112
|
+
if (!source)
|
|
113
|
+
return null;
|
|
114
|
+
return {
|
|
115
|
+
source,
|
|
116
|
+
medium: normalize(params.get("utm_medium")),
|
|
117
|
+
campaign: normalize(params.get("utm_campaign")),
|
|
118
|
+
content: normalize(params.get("utm_content")),
|
|
119
|
+
term: normalize(params.get("utm_term"))
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function parseReferrer(referrer, currentHostname, referrerMap = []) {
|
|
123
|
+
if (!referrer)
|
|
124
|
+
return null;
|
|
125
|
+
try {
|
|
126
|
+
const hostname = new URL(referrer).hostname.toLowerCase();
|
|
127
|
+
if (hostname === currentHostname.toLowerCase())
|
|
128
|
+
return null;
|
|
129
|
+
const match = referrerMap.find((rule) => rule.pattern.test(hostname));
|
|
130
|
+
return match ? { source: match.source, medium: match.medium } : { source: hostname, medium: "referral" };
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function readStored(storage, key, now, ttlMs) {
|
|
136
|
+
try {
|
|
137
|
+
const raw = storage.getItem(key);
|
|
138
|
+
if (!raw)
|
|
139
|
+
return null;
|
|
140
|
+
const parsed = StoredAttributionSchema.safeParse(JSON.parse(raw));
|
|
141
|
+
if (!parsed.success || now - parsed.data.firstTouch.capturedAt > ttlMs) {
|
|
142
|
+
storage.removeItem(key);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
return parsed.data;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function writeStored(storage, key, value) {
|
|
151
|
+
try {
|
|
152
|
+
storage.setItem(key, JSON.stringify(value));
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
155
|
+
function resolveAttribution({
|
|
156
|
+
search,
|
|
157
|
+
referrer,
|
|
158
|
+
pathname,
|
|
159
|
+
hostname,
|
|
160
|
+
storage,
|
|
161
|
+
referrerMap = [],
|
|
162
|
+
storageKey = "tracking.attribution",
|
|
163
|
+
ttlMs = 90 * 24 * 60 * 60 * 1000,
|
|
164
|
+
now = Date.now()
|
|
165
|
+
}) {
|
|
166
|
+
const stored = readStored(storage, storageKey, now, ttlMs);
|
|
167
|
+
const urlUtm = parseUtmFromSearch(search);
|
|
168
|
+
if (urlUtm) {
|
|
169
|
+
const currentTouch = {
|
|
170
|
+
utm: urlUtm,
|
|
171
|
+
referrer: referrer || undefined,
|
|
172
|
+
landingPage: pathname
|
|
173
|
+
};
|
|
174
|
+
const persisted = {
|
|
175
|
+
firstTouch: {
|
|
176
|
+
data: stored?.firstTouch.data ?? currentTouch,
|
|
177
|
+
capturedAt: stored?.firstTouch.capturedAt ?? now
|
|
178
|
+
},
|
|
179
|
+
currentTouch: { data: currentTouch, capturedAt: now }
|
|
180
|
+
};
|
|
181
|
+
writeStored(storage, storageKey, persisted);
|
|
182
|
+
return {
|
|
183
|
+
firstTouch: persisted.firstTouch.data,
|
|
184
|
+
currentTouch: persisted.currentTouch.data
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (stored)
|
|
188
|
+
return { firstTouch: stored.firstTouch.data, currentTouch: stored.currentTouch.data };
|
|
189
|
+
const fromReferrer = parseReferrer(referrer, hostname, referrerMap);
|
|
190
|
+
const firstTouch = {
|
|
191
|
+
utm: fromReferrer ? { source: fromReferrer.source, medium: fromReferrer.medium } : undefined,
|
|
192
|
+
referrer: referrer || undefined,
|
|
193
|
+
landingPage: pathname
|
|
194
|
+
};
|
|
195
|
+
writeStored(storage, storageKey, {
|
|
196
|
+
firstTouch: { data: firstTouch, capturedAt: now },
|
|
197
|
+
currentTouch: { data: firstTouch, capturedAt: now }
|
|
198
|
+
});
|
|
199
|
+
return { firstTouch, currentTouch: firstTouch };
|
|
200
|
+
}
|
|
201
|
+
// src/tracking/beacon.ts
|
|
202
|
+
function sendUnloadBeacon(url, body, sender = typeof navigator === "undefined" ? undefined : navigator) {
|
|
203
|
+
if (!sender || typeof sender.sendBeacon !== "function")
|
|
204
|
+
return false;
|
|
205
|
+
return sender.sendBeacon(url, body);
|
|
206
|
+
}
|
|
207
|
+
// src/tracking/clicks.ts
|
|
208
|
+
function label(target) {
|
|
209
|
+
return (target?.textContent ?? "").trim().slice(0, 100) || undefined;
|
|
210
|
+
}
|
|
211
|
+
function sanitizedHref(value, origin) {
|
|
212
|
+
if (!value)
|
|
213
|
+
return;
|
|
214
|
+
try {
|
|
215
|
+
const url = new URL(value, origin);
|
|
216
|
+
return url.origin === origin ? url.pathname : `${url.origin}${url.pathname}`;
|
|
217
|
+
} catch {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function resolveTrackedClick(target, { origin, attributes = {}, isAction = () => true }) {
|
|
222
|
+
if (!target)
|
|
223
|
+
return null;
|
|
224
|
+
const trackAttribute = attributes.track ?? "data-track";
|
|
225
|
+
const actionAttribute = attributes.action ?? "data-track-action";
|
|
226
|
+
const contextAttribute = attributes.context ?? "data-track-context";
|
|
227
|
+
const tracked = target.closest(`[${trackAttribute}], [${actionAttribute}]`);
|
|
228
|
+
const anchor = target.closest("a[href]");
|
|
229
|
+
const href = anchor?.getAttribute("href") ?? tracked?.getAttribute("href") ?? null;
|
|
230
|
+
let sameOriginNavigation = false;
|
|
231
|
+
let outbound = false;
|
|
232
|
+
if (href) {
|
|
233
|
+
try {
|
|
234
|
+
const url = new URL(href, origin);
|
|
235
|
+
sameOriginNavigation = url.origin === origin;
|
|
236
|
+
outbound = url.protocol.startsWith("http") && !sameOriginNavigation;
|
|
237
|
+
} catch {
|
|
238
|
+
sameOriginNavigation = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const result = { leavesPage: sameOriginNavigation || outbound };
|
|
242
|
+
if (tracked) {
|
|
243
|
+
const action = tracked.getAttribute(actionAttribute);
|
|
244
|
+
const element = tracked.getAttribute(trackAttribute);
|
|
245
|
+
if (action && isAction(action)) {
|
|
246
|
+
result.interaction = {
|
|
247
|
+
action,
|
|
248
|
+
context: tracked.getAttribute(contextAttribute) ?? undefined
|
|
249
|
+
};
|
|
250
|
+
} else if (element) {
|
|
251
|
+
result.click = {
|
|
252
|
+
element,
|
|
253
|
+
elementText: label(tracked),
|
|
254
|
+
href: sanitizedHref(href, origin)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (outbound && href) {
|
|
259
|
+
result.outbound = { href: sanitizedHref(href, origin) ?? href, label: label(anchor) };
|
|
260
|
+
}
|
|
261
|
+
return result.interaction || result.click || result.outbound ? result : null;
|
|
262
|
+
}
|
|
263
|
+
// src/tracking/delivery.ts
|
|
264
|
+
function isRetryable(error) {
|
|
265
|
+
return ApiError.is(error) && (error.status === 0 || error.status >= 500);
|
|
266
|
+
}
|
|
267
|
+
function isUnauthorized(error) {
|
|
268
|
+
return ApiError.is(error) && (error.status === 401 || error.status === 403);
|
|
269
|
+
}
|
|
270
|
+
async function deliverTrackingBatch({
|
|
271
|
+
request,
|
|
272
|
+
onFailure,
|
|
273
|
+
onUnauthorized,
|
|
274
|
+
wait = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
|
|
275
|
+
retryDelayMs = 250
|
|
276
|
+
}) {
|
|
277
|
+
let recovered = false;
|
|
278
|
+
for (let attempt = 0;attempt < 2; attempt += 1) {
|
|
279
|
+
try {
|
|
280
|
+
await request();
|
|
281
|
+
return "delivered";
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (attempt === 0 && isRetryable(error)) {
|
|
284
|
+
await wait(retryDelayMs);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (isUnauthorized(error) && onUnauthorized && !recovered) {
|
|
288
|
+
recovered = true;
|
|
289
|
+
if (await onUnauthorized())
|
|
290
|
+
continue;
|
|
291
|
+
return "auth-invalidated";
|
|
292
|
+
}
|
|
293
|
+
onFailure(error);
|
|
294
|
+
return "failed";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return "failed";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/tracking/scroll.ts
|
|
301
|
+
function scrollDepthPercent(view) {
|
|
302
|
+
const scrollable = view.scrollHeight - view.innerHeight;
|
|
303
|
+
if (scrollable <= 0)
|
|
304
|
+
return 0;
|
|
305
|
+
return Math.max(0, Math.min(100, Math.round(view.scrollY / scrollable * 100)));
|
|
306
|
+
}
|
|
307
|
+
function createScrollMilestones(milestones = [25, 50, 75, 100]) {
|
|
308
|
+
const sorted = [...milestones].sort((a, b) => a - b);
|
|
309
|
+
let deepest = 0;
|
|
310
|
+
const fired = new Set;
|
|
311
|
+
return {
|
|
312
|
+
record(percent) {
|
|
313
|
+
deepest = Math.max(deepest, percent);
|
|
314
|
+
},
|
|
315
|
+
observe(percent) {
|
|
316
|
+
deepest = Math.max(deepest, percent);
|
|
317
|
+
const reached = [];
|
|
318
|
+
for (const milestone of sorted) {
|
|
319
|
+
if (deepest >= milestone && !fired.has(milestone)) {
|
|
320
|
+
fired.add(milestone);
|
|
321
|
+
reached.push(milestone);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return reached;
|
|
325
|
+
},
|
|
326
|
+
max: () => deepest,
|
|
327
|
+
reset() {
|
|
328
|
+
deepest = 0;
|
|
329
|
+
fired.clear();
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/tracking/sequence-reserve.ts
|
|
335
|
+
function createSequenceReserve(reserve, options = {}) {
|
|
336
|
+
const blockSize = options.blockSize ?? 16;
|
|
337
|
+
const lowWater = options.lowWater ?? 4;
|
|
338
|
+
const pool = [];
|
|
339
|
+
let pending = null;
|
|
340
|
+
let fallback = null;
|
|
341
|
+
const pushFallbackBlock = () => {
|
|
342
|
+
for (let i = 0;i < blockSize; i += 1) {
|
|
343
|
+
fallback = (fallback ?? 0) + 1;
|
|
344
|
+
pool.push(fallback);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const refill = () => {
|
|
348
|
+
if (pending)
|
|
349
|
+
return pending;
|
|
350
|
+
if (fallback !== null) {
|
|
351
|
+
pushFallbackBlock();
|
|
352
|
+
return Promise.resolve();
|
|
353
|
+
}
|
|
354
|
+
pending = reserve(blockSize).then((reserved) => {
|
|
355
|
+
pool.push(...reserved);
|
|
356
|
+
}).catch((error) => {
|
|
357
|
+
fallback = (options.fallbackBase ?? (() => Math.floor(Math.random() * 1e6)))() * 1e6;
|
|
358
|
+
options.onUnavailable?.(error);
|
|
359
|
+
pushFallbackBlock();
|
|
360
|
+
}).finally(() => {
|
|
361
|
+
pending = null;
|
|
362
|
+
});
|
|
363
|
+
return pending;
|
|
364
|
+
};
|
|
365
|
+
return {
|
|
366
|
+
take() {
|
|
367
|
+
const next = pool.shift() ?? null;
|
|
368
|
+
if (pool.length < lowWater)
|
|
369
|
+
refill();
|
|
370
|
+
return next;
|
|
371
|
+
},
|
|
372
|
+
refill,
|
|
373
|
+
shared: () => fallback === null,
|
|
374
|
+
reset() {
|
|
375
|
+
fallback = null;
|
|
376
|
+
pool.length = 0;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/tracking/visible-time.ts
|
|
382
|
+
function createVisibleTimeMeter(options = {}) {
|
|
383
|
+
const now = options.now ?? (() => performance.now());
|
|
384
|
+
const wallClock = options.wallClock ?? (() => Date.now());
|
|
385
|
+
const randomUUID = options.randomUUID ?? (() => crypto.randomUUID());
|
|
386
|
+
let checkpointAt = now();
|
|
387
|
+
const cut = () => {
|
|
388
|
+
const at = now();
|
|
389
|
+
const activeDurationMs = Math.max(0, Math.round(at - checkpointAt));
|
|
390
|
+
checkpointAt = at;
|
|
391
|
+
return {
|
|
392
|
+
activeIntervalId: randomUUID(),
|
|
393
|
+
activeDurationMs,
|
|
394
|
+
intervalStartedAt: wallClock() - activeDurationMs
|
|
395
|
+
};
|
|
396
|
+
};
|
|
397
|
+
return {
|
|
398
|
+
checkpoint() {
|
|
399
|
+
checkpointAt = now();
|
|
400
|
+
},
|
|
401
|
+
cut,
|
|
402
|
+
heartbeat(visible) {
|
|
403
|
+
if (visible)
|
|
404
|
+
return { ...cut(), isVisible: true };
|
|
405
|
+
return { isVisible: false, activeDurationMs: 0 };
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/tracking/client.ts
|
|
411
|
+
var CONVENTIONAL_TRACKING_EVENT_TYPES = {
|
|
412
|
+
pageView: "PAGE_VIEW",
|
|
413
|
+
pageLeave: "PAGE_LEAVE",
|
|
414
|
+
scrollDepth: "SCROLL_DEPTH",
|
|
415
|
+
heartbeat: "SESSION_HEARTBEAT",
|
|
416
|
+
click: "CLICK",
|
|
417
|
+
outboundClick: "OUTBOUND_CLICK",
|
|
418
|
+
interaction: "INTERACTION"
|
|
419
|
+
};
|
|
420
|
+
function chunk(items, size) {
|
|
421
|
+
const chunks = [];
|
|
422
|
+
for (let at = 0;at < items.length; at += size)
|
|
423
|
+
chunks.push(items.slice(at, at + size));
|
|
424
|
+
return chunks;
|
|
425
|
+
}
|
|
426
|
+
function createTrackingClient(config) {
|
|
427
|
+
if (typeof config.buildId !== "string" || config.buildId.length === 0) {
|
|
428
|
+
throw new Error('[stitchkit] tracking client requires a buildId — a git SHA or "dev"');
|
|
429
|
+
}
|
|
430
|
+
const { host, builtin, outbox } = config;
|
|
431
|
+
const heartbeatMs = config.heartbeatMs ?? 30000;
|
|
432
|
+
const renewAfterHiddenMs = config.renewAfterHiddenMs ?? 30 * 60 * 1000;
|
|
433
|
+
const pendingLimit = config.pendingLimit ?? 100;
|
|
434
|
+
const batchSize = config.batchSize ?? 50;
|
|
435
|
+
const fail = config.onFailure ?? (() => {
|
|
436
|
+
return;
|
|
437
|
+
});
|
|
438
|
+
const decorate = config.decorate ?? ((event) => event);
|
|
439
|
+
const unload = config.unload ?? ((request) => config.unloadUrl === undefined ? false : sendUnloadBeacon(config.unloadUrl, JSON.stringify(request)));
|
|
440
|
+
let browserStreamId = null;
|
|
441
|
+
let visitId = null;
|
|
442
|
+
let outboxAvailable = outbox !== undefined;
|
|
443
|
+
let bootstrapping = null;
|
|
444
|
+
let pending = [];
|
|
445
|
+
let currentPage = null;
|
|
446
|
+
let attribution = null;
|
|
447
|
+
let running = 0;
|
|
448
|
+
const flushOwner = host.randomUUID();
|
|
449
|
+
const meter = createVisibleTimeMeter({
|
|
450
|
+
now: () => host.now(),
|
|
451
|
+
wallClock: () => host.wallClock(),
|
|
452
|
+
randomUUID: () => host.randomUUID()
|
|
453
|
+
});
|
|
454
|
+
const scroll = createScrollMilestones(config.scrollMilestones);
|
|
455
|
+
const sequences = createSequenceReserve((count) => {
|
|
456
|
+
if (!outbox || !outboxAvailable)
|
|
457
|
+
return Promise.reject(new Error("outbox unavailable"));
|
|
458
|
+
return outbox.reserveSequences(count);
|
|
459
|
+
}, {
|
|
460
|
+
...config.sequence,
|
|
461
|
+
onUnavailable: outbox ? (error) => {
|
|
462
|
+
outboxAvailable = false;
|
|
463
|
+
fail("sequence reservation", error);
|
|
464
|
+
} : undefined
|
|
465
|
+
});
|
|
466
|
+
const currentUtm = () => attribution?.currentTouch.utm?.source ? attribution.currentTouch.utm : undefined;
|
|
467
|
+
const captureAttribution = (search, pathname) => {
|
|
468
|
+
if (!host.storage)
|
|
469
|
+
return null;
|
|
470
|
+
const page = host.page();
|
|
471
|
+
attribution = resolveAttribution({
|
|
472
|
+
search,
|
|
473
|
+
pathname,
|
|
474
|
+
referrer: page.referrer,
|
|
475
|
+
hostname: page.hostname,
|
|
476
|
+
storage: host.storage,
|
|
477
|
+
referrerMap: config.referrerMap,
|
|
478
|
+
now: host.wallClock()
|
|
479
|
+
});
|
|
480
|
+
return attribution;
|
|
481
|
+
};
|
|
482
|
+
const bootstrapVisit = (renew = false) => {
|
|
483
|
+
if (!renew && visitId && browserStreamId)
|
|
484
|
+
return Promise.resolve(visitId);
|
|
485
|
+
if (bootstrapping)
|
|
486
|
+
return bootstrapping;
|
|
487
|
+
const run = (async () => {
|
|
488
|
+
try {
|
|
489
|
+
const health = outbox ? await outbox.health() : { state: "unavailable", queued: 0, dropped: 0 };
|
|
490
|
+
const wasAvailable = outboxAvailable;
|
|
491
|
+
outboxAvailable = health.state === "available";
|
|
492
|
+
if (outboxAvailable && !wasAvailable && !sequences.shared())
|
|
493
|
+
sequences.reset();
|
|
494
|
+
browserStreamId = outboxAvailable && outbox ? await outbox.streamId() : browserStreamId ?? host.randomUUID();
|
|
495
|
+
await sequences.refill();
|
|
496
|
+
const page = host.page();
|
|
497
|
+
const touch = captureAttribution(page.search, page.pathname);
|
|
498
|
+
const response = await config.bootstrap({
|
|
499
|
+
browserStreamId,
|
|
500
|
+
previousVisitId: visitId ?? undefined,
|
|
501
|
+
origin: page.origin,
|
|
502
|
+
landingPath: page.pathname,
|
|
503
|
+
referrer: page.referrer || undefined,
|
|
504
|
+
utm: touch?.currentTouch.utm?.source ? touch.currentTouch.utm : undefined,
|
|
505
|
+
displayMode: page.displayMode,
|
|
506
|
+
screenWidth: page.screenWidth,
|
|
507
|
+
screenHeight: page.screenHeight,
|
|
508
|
+
buildId: config.buildId,
|
|
509
|
+
outboxState: health.state,
|
|
510
|
+
outboxQueued: health.queued,
|
|
511
|
+
outboxDropped: health.dropped
|
|
512
|
+
});
|
|
513
|
+
visitId = response.visitId;
|
|
514
|
+
config.onVisit?.(visitId);
|
|
515
|
+
return visitId;
|
|
516
|
+
} catch (error) {
|
|
517
|
+
fail("visit bootstrap", error);
|
|
518
|
+
return null;
|
|
519
|
+
} finally {
|
|
520
|
+
bootstrapping = null;
|
|
521
|
+
}
|
|
522
|
+
})();
|
|
523
|
+
bootstrapping = run;
|
|
524
|
+
return run;
|
|
525
|
+
};
|
|
526
|
+
const deliver = async (events) => {
|
|
527
|
+
let response = null;
|
|
528
|
+
const outcome = await deliverTrackingBatch({
|
|
529
|
+
request: async () => {
|
|
530
|
+
response = await config.deliver({
|
|
531
|
+
buildId: config.buildId,
|
|
532
|
+
events,
|
|
533
|
+
utm: currentUtm()
|
|
534
|
+
});
|
|
535
|
+
},
|
|
536
|
+
onUnauthorized: config.onUnauthorized,
|
|
537
|
+
onFailure: (error) => fail("event delivery", error)
|
|
538
|
+
});
|
|
539
|
+
return outcome === "delivered" ? response : null;
|
|
540
|
+
};
|
|
541
|
+
let flushing = null;
|
|
542
|
+
let queued = null;
|
|
543
|
+
const flushOutbox = () => {
|
|
544
|
+
if (!outbox || !outboxAvailable)
|
|
545
|
+
return Promise.resolve();
|
|
546
|
+
if (flushing) {
|
|
547
|
+
queued ??= flushing.then(() => {
|
|
548
|
+
queued = null;
|
|
549
|
+
return flushOutbox();
|
|
550
|
+
});
|
|
551
|
+
return queued;
|
|
552
|
+
}
|
|
553
|
+
flushing = (async () => {
|
|
554
|
+
try {
|
|
555
|
+
if (!await outbox.acquireLease(flushOwner))
|
|
556
|
+
return;
|
|
557
|
+
const batch = await outbox.readBatch(batchSize);
|
|
558
|
+
if (batch.length === 0)
|
|
559
|
+
return;
|
|
560
|
+
const response = await deliver(batch);
|
|
561
|
+
if (response) {
|
|
562
|
+
await outbox.acknowledge(response.dispositions.map((item) => item.eventId));
|
|
563
|
+
}
|
|
564
|
+
} catch (error) {
|
|
565
|
+
fail("outbox flush", error);
|
|
566
|
+
} finally {
|
|
567
|
+
flushing = null;
|
|
568
|
+
}
|
|
569
|
+
})();
|
|
570
|
+
return flushing;
|
|
571
|
+
};
|
|
572
|
+
const materialize = (draft2) => {
|
|
573
|
+
if (!visitId || !browserStreamId)
|
|
574
|
+
return null;
|
|
575
|
+
const sequence = sequences.take();
|
|
576
|
+
if (sequence === null)
|
|
577
|
+
return null;
|
|
578
|
+
return decorate({
|
|
579
|
+
...draft2,
|
|
580
|
+
eventId: host.randomUUID(),
|
|
581
|
+
visitId,
|
|
582
|
+
browserStreamId,
|
|
583
|
+
browserSequence: sequence
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
const persistAndFlush = async (events) => {
|
|
587
|
+
if (!outbox || !outboxAvailable) {
|
|
588
|
+
for (const batch of chunk(events, batchSize))
|
|
589
|
+
await deliver(batch);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
for (const event of events)
|
|
594
|
+
await outbox.enqueue(event);
|
|
595
|
+
await flushOutbox();
|
|
596
|
+
} catch (error) {
|
|
597
|
+
fail("event persistence", error);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
const drainPending = () => {
|
|
601
|
+
const drafts = pending.splice(0);
|
|
602
|
+
if (drafts.length > 0)
|
|
603
|
+
send(drafts);
|
|
604
|
+
};
|
|
605
|
+
const send = (drafts) => {
|
|
606
|
+
if (!visitId || !browserStreamId) {
|
|
607
|
+
pending = [...pending, ...drafts].slice(-pendingLimit);
|
|
608
|
+
bootstrapVisit().then((issued) => {
|
|
609
|
+
if (issued)
|
|
610
|
+
drainPending();
|
|
611
|
+
});
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
(async () => {
|
|
615
|
+
const events = [];
|
|
616
|
+
for (const draft2 of drafts) {
|
|
617
|
+
let event = materialize(draft2);
|
|
618
|
+
if (!event) {
|
|
619
|
+
await sequences.refill();
|
|
620
|
+
event = materialize(draft2);
|
|
621
|
+
}
|
|
622
|
+
if (event)
|
|
623
|
+
events.push(event);
|
|
624
|
+
}
|
|
625
|
+
if (events.length > 0)
|
|
626
|
+
await persistAndFlush(events);
|
|
627
|
+
})();
|
|
628
|
+
};
|
|
629
|
+
const sendOnUnload = (draft2) => {
|
|
630
|
+
const event = materialize(draft2);
|
|
631
|
+
if (!event) {
|
|
632
|
+
send([draft2]);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
unload({ buildId: config.buildId, events: [event], utm: currentUtm() });
|
|
636
|
+
if (outbox && outboxAvailable) {
|
|
637
|
+
outbox.enqueue(event).catch((error) => fail("unload event persistence", error));
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
const draft = (type, metadata) => ({
|
|
641
|
+
type,
|
|
642
|
+
page: currentPage ?? host.page().pathname,
|
|
643
|
+
metadata,
|
|
644
|
+
clientTimestamp: host.wallClock()
|
|
645
|
+
});
|
|
646
|
+
const leaveDraft = (page) => ({
|
|
647
|
+
type: builtin.pageLeave,
|
|
648
|
+
page,
|
|
649
|
+
metadata: { ...meter.cut(), scrollDepthPercent: scroll.max() },
|
|
650
|
+
clientTimestamp: host.wallClock()
|
|
651
|
+
});
|
|
652
|
+
function track(type, metadata) {
|
|
653
|
+
send([draft(type, metadata === undefined ? undefined : asMetadata(metadata))]);
|
|
654
|
+
}
|
|
655
|
+
const onNavigate = (pathname, search) => {
|
|
656
|
+
captureAttribution(search, pathname);
|
|
657
|
+
const previous = currentPage;
|
|
658
|
+
if (previous === pathname)
|
|
659
|
+
return;
|
|
660
|
+
if (previous && host.visible())
|
|
661
|
+
send([leaveDraft(previous)]);
|
|
662
|
+
currentPage = pathname;
|
|
663
|
+
meter.checkpoint();
|
|
664
|
+
scroll.reset();
|
|
665
|
+
const page = host.page();
|
|
666
|
+
send([
|
|
667
|
+
{
|
|
668
|
+
type: builtin.pageView,
|
|
669
|
+
page: pathname,
|
|
670
|
+
metadata: {
|
|
671
|
+
title: page.title,
|
|
672
|
+
screenWidth: page.viewportWidth,
|
|
673
|
+
screenHeight: page.viewportHeight,
|
|
674
|
+
referrer: previous ? undefined : page.referrer || undefined
|
|
675
|
+
},
|
|
676
|
+
clientTimestamp: host.wallClock()
|
|
677
|
+
}
|
|
678
|
+
]);
|
|
679
|
+
};
|
|
680
|
+
const start = () => {
|
|
681
|
+
const generation = ++running;
|
|
682
|
+
const unsubscribe = [];
|
|
683
|
+
bootstrapVisit().then((issued) => {
|
|
684
|
+
if (!issued || generation !== running)
|
|
685
|
+
return;
|
|
686
|
+
drainPending();
|
|
687
|
+
flushOutbox();
|
|
688
|
+
});
|
|
689
|
+
const renewThenFlush = () => void bootstrapVisit(true).then(() => flushOutbox());
|
|
690
|
+
unsubscribe.push(host.on("online", renewThenFlush));
|
|
691
|
+
unsubscribe.push(host.interval(() => void flushOutbox(), heartbeatMs));
|
|
692
|
+
let hiddenAt = null;
|
|
693
|
+
let lastLeaveAt = Number.NEGATIVE_INFINITY;
|
|
694
|
+
const onHide = () => {
|
|
695
|
+
const at = host.now();
|
|
696
|
+
if (at - lastLeaveAt < 1000)
|
|
697
|
+
return;
|
|
698
|
+
lastLeaveAt = at;
|
|
699
|
+
sendOnUnload(leaveDraft(currentPage ?? host.page().pathname));
|
|
700
|
+
if (outbox && outboxAvailable)
|
|
701
|
+
outbox.releaseLease(flushOwner).catch(() => {
|
|
702
|
+
return;
|
|
703
|
+
});
|
|
704
|
+
};
|
|
705
|
+
unsubscribe.push(host.on("pagehide", onHide));
|
|
706
|
+
unsubscribe.push(host.on("visibilitychange", () => {
|
|
707
|
+
if (!host.visible()) {
|
|
708
|
+
hiddenAt = host.wallClock();
|
|
709
|
+
onHide();
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
meter.checkpoint();
|
|
713
|
+
if (hiddenAt !== null && host.wallClock() - hiddenAt >= renewAfterHiddenMs) {
|
|
714
|
+
renewThenFlush();
|
|
715
|
+
}
|
|
716
|
+
hiddenAt = null;
|
|
717
|
+
}));
|
|
718
|
+
unsubscribe.push(host.interval(() => {
|
|
719
|
+
send([draft(builtin.heartbeat, { ...meter.heartbeat(host.visible()) })]);
|
|
720
|
+
}, heartbeatMs));
|
|
721
|
+
unsubscribe.push(host.on("scroll", () => scroll.record(host.scrollDepth())));
|
|
722
|
+
unsubscribe.push(host.interval(() => {
|
|
723
|
+
for (const milestone of scroll.observe(host.scrollDepth())) {
|
|
724
|
+
send([draft(builtin.scrollDepth, { maxPercent: milestone })]);
|
|
725
|
+
}
|
|
726
|
+
}, 2000));
|
|
727
|
+
unsubscribe.push(host.onClick((target) => {
|
|
728
|
+
const click = resolveTrackedClick(target, {
|
|
729
|
+
origin: host.page().origin,
|
|
730
|
+
attributes: config.clickAttributes,
|
|
731
|
+
isAction: config.isAction
|
|
732
|
+
});
|
|
733
|
+
if (!click)
|
|
734
|
+
return;
|
|
735
|
+
const drafts = [];
|
|
736
|
+
if (click.interaction)
|
|
737
|
+
drafts.push(draft(builtin.interaction, { ...click.interaction }));
|
|
738
|
+
if (click.click)
|
|
739
|
+
drafts.push(draft(builtin.click, { ...click.click }));
|
|
740
|
+
if (click.outbound)
|
|
741
|
+
drafts.push(draft(builtin.outboundClick, { ...click.outbound }));
|
|
742
|
+
if (click.leavesPage)
|
|
743
|
+
for (const item of drafts)
|
|
744
|
+
sendOnUnload(item);
|
|
745
|
+
else
|
|
746
|
+
send(drafts);
|
|
747
|
+
}));
|
|
748
|
+
return () => {
|
|
749
|
+
running += 1;
|
|
750
|
+
for (const off of unsubscribe.splice(0))
|
|
751
|
+
off();
|
|
752
|
+
};
|
|
753
|
+
};
|
|
754
|
+
return {
|
|
755
|
+
track,
|
|
756
|
+
onNavigate,
|
|
757
|
+
start,
|
|
758
|
+
visitId: () => visitId,
|
|
759
|
+
browserStreamId: () => browserStreamId,
|
|
760
|
+
attribution: () => attribution
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
function asMetadata(value) {
|
|
764
|
+
return typeof value === "object" && value !== null ? { ...value } : undefined;
|
|
765
|
+
}
|
|
766
|
+
// src/tracking/contract.ts
|
|
767
|
+
function createTrackingContract(config) {
|
|
768
|
+
const schemas = createTrackingSchemas(config);
|
|
769
|
+
return defineContract({ prefix: config.prefix ?? "tracking", scope: config.scope }, {
|
|
770
|
+
bootstrap: {
|
|
771
|
+
method: "POST",
|
|
772
|
+
path: "/visit",
|
|
773
|
+
expose: ["HTTP"],
|
|
774
|
+
desc: "Issue or renew a browser visit lease",
|
|
775
|
+
input: schemas.entry,
|
|
776
|
+
output: schemas.bootstrap
|
|
777
|
+
},
|
|
778
|
+
track: {
|
|
779
|
+
method: "POST",
|
|
780
|
+
path: "/events",
|
|
781
|
+
desc: "Batch track browser events",
|
|
782
|
+
input: schemas.request,
|
|
783
|
+
output: schemas.response,
|
|
784
|
+
safelistedBody: true,
|
|
785
|
+
maxJsonBodyBytes: config.maxJsonBodyBytes ?? 256 * 1024
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
// src/tracking/host.ts
|
|
790
|
+
function browserTrackingHost() {
|
|
791
|
+
return {
|
|
792
|
+
page: () => ({
|
|
793
|
+
pathname: window.location.pathname,
|
|
794
|
+
search: window.location.search,
|
|
795
|
+
origin: window.location.origin,
|
|
796
|
+
hostname: window.location.hostname,
|
|
797
|
+
title: document.title,
|
|
798
|
+
referrer: document.referrer,
|
|
799
|
+
viewportWidth: window.innerWidth,
|
|
800
|
+
viewportHeight: window.innerHeight,
|
|
801
|
+
screenWidth: window.screen.width,
|
|
802
|
+
screenHeight: window.screen.height,
|
|
803
|
+
displayMode: window.matchMedia("(display-mode: standalone)").matches ? "standalone" : "browser"
|
|
804
|
+
}),
|
|
805
|
+
visible: () => document.visibilityState === "visible",
|
|
806
|
+
scrollDepth() {
|
|
807
|
+
const scrollable = document.documentElement.scrollHeight - window.innerHeight;
|
|
808
|
+
if (scrollable <= 0)
|
|
809
|
+
return 0;
|
|
810
|
+
return Math.max(0, Math.min(100, Math.round(window.scrollY / scrollable * 100)));
|
|
811
|
+
},
|
|
812
|
+
on(event, handler) {
|
|
813
|
+
const target = event === "visibilitychange" ? document : window;
|
|
814
|
+
const options = event === "scroll" ? { passive: true } : undefined;
|
|
815
|
+
target.addEventListener(event, handler, options);
|
|
816
|
+
return () => target.removeEventListener(event, handler);
|
|
817
|
+
},
|
|
818
|
+
onClick(handler) {
|
|
819
|
+
const listener = (event) => {
|
|
820
|
+
handler(event.target instanceof Element ? event.target : null);
|
|
821
|
+
};
|
|
822
|
+
document.addEventListener("click", listener, { capture: true });
|
|
823
|
+
return () => document.removeEventListener("click", listener, { capture: true });
|
|
824
|
+
},
|
|
825
|
+
interval(handler, ms) {
|
|
826
|
+
const id = window.setInterval(handler, ms);
|
|
827
|
+
return () => window.clearInterval(id);
|
|
828
|
+
},
|
|
829
|
+
now: () => performance.now(),
|
|
830
|
+
wallClock: () => Date.now(),
|
|
831
|
+
randomUUID: () => crypto.randomUUID(),
|
|
832
|
+
storage: (() => {
|
|
833
|
+
try {
|
|
834
|
+
return window.localStorage;
|
|
835
|
+
} catch {
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
})()
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
// src/tracking/once-per-page.ts
|
|
842
|
+
function createOncePerPage(options = {}) {
|
|
843
|
+
const windowMs = options.windowMs ?? 15000;
|
|
844
|
+
const now = options.now ?? (() => Date.now());
|
|
845
|
+
const seen = new Map;
|
|
846
|
+
return {
|
|
847
|
+
should(key, page) {
|
|
848
|
+
const scoped = `${page}|${key}`;
|
|
849
|
+
const at = now();
|
|
850
|
+
const last = seen.get(scoped);
|
|
851
|
+
if (last !== undefined && at - last < windowMs)
|
|
852
|
+
return false;
|
|
853
|
+
seen.set(scoped, at);
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
// src/tracking/outbox.ts
|
|
859
|
+
var META_STREAM = "streamId";
|
|
860
|
+
var META_SEQUENCE = "sequence";
|
|
861
|
+
var META_LEASE = "lease";
|
|
862
|
+
var META_DROPPED = "dropped";
|
|
863
|
+
function numberOr(value, fallback) {
|
|
864
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
865
|
+
}
|
|
866
|
+
function leaseOf(raw) {
|
|
867
|
+
if (typeof raw !== "object" || raw === null)
|
|
868
|
+
return null;
|
|
869
|
+
const owner = Reflect.get(raw, "owner");
|
|
870
|
+
const expiresAt = Reflect.get(raw, "expiresAt");
|
|
871
|
+
return typeof owner === "string" && typeof expiresAt === "number" ? { owner, expiresAt } : null;
|
|
872
|
+
}
|
|
873
|
+
function createTrackingOutbox(storage, options = {}) {
|
|
874
|
+
const maxEvents = options.maxEvents ?? 1000;
|
|
875
|
+
const maxAgeMs = options.maxAgeMs ?? 7 * 24 * 60 * 60 * 1000;
|
|
876
|
+
const leaseMs = options.leaseMs ?? 3000;
|
|
877
|
+
const now = options.now ?? (() => Date.now());
|
|
878
|
+
const randomUUID = options.randomUUID ?? (() => crypto.randomUUID());
|
|
879
|
+
const trim = () => storage.transact(async ({ meta, events }) => {
|
|
880
|
+
const all = await events.all();
|
|
881
|
+
const cutoff = now() - maxAgeMs;
|
|
882
|
+
const remove = all.filter((record, index) => record.enqueuedAt < cutoff || index < all.length - maxEvents);
|
|
883
|
+
for (const record of remove)
|
|
884
|
+
events.delete(record.event.eventId);
|
|
885
|
+
if (remove.length > 0) {
|
|
886
|
+
meta.put(META_DROPPED, numberOr(await meta.get(META_DROPPED), 0) + remove.length);
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
return {
|
|
890
|
+
streamId: () => storage.transact(async ({ meta }) => {
|
|
891
|
+
const existing = await meta.get(META_STREAM);
|
|
892
|
+
if (typeof existing === "string")
|
|
893
|
+
return existing;
|
|
894
|
+
const created = randomUUID();
|
|
895
|
+
meta.put(META_STREAM, created);
|
|
896
|
+
return created;
|
|
897
|
+
}),
|
|
898
|
+
reserveSequences: (count) => storage.transact(async ({ meta }) => {
|
|
899
|
+
const start = numberOr(await meta.get(META_SEQUENCE), 0) + 1;
|
|
900
|
+
meta.put(META_SEQUENCE, start + count - 1);
|
|
901
|
+
return Array.from({ length: count }, (_, index) => start + index);
|
|
902
|
+
}),
|
|
903
|
+
async enqueue(event) {
|
|
904
|
+
await storage.transact(async ({ events }) => {
|
|
905
|
+
events.put({ event, enqueuedAt: now() });
|
|
906
|
+
});
|
|
907
|
+
await trim();
|
|
908
|
+
},
|
|
909
|
+
acquireLease: (owner) => storage.transact(async ({ meta }) => {
|
|
910
|
+
const lease = leaseOf(await meta.get(META_LEASE));
|
|
911
|
+
const at = now();
|
|
912
|
+
const acquired = lease === null || lease.owner === owner || lease.expiresAt <= at;
|
|
913
|
+
if (acquired)
|
|
914
|
+
meta.put(META_LEASE, { owner, expiresAt: at + leaseMs });
|
|
915
|
+
return acquired;
|
|
916
|
+
}),
|
|
917
|
+
releaseLease: (owner) => storage.transact(async ({ meta }) => {
|
|
918
|
+
const lease = leaseOf(await meta.get(META_LEASE));
|
|
919
|
+
if (lease?.owner === owner)
|
|
920
|
+
meta.delete(META_LEASE);
|
|
921
|
+
}),
|
|
922
|
+
readBatch: (limit = 50) => storage.transact(async ({ events }) => (await events.all()).slice(0, limit).map((record) => record.event), "readonly"),
|
|
923
|
+
acknowledge: async (eventIds) => {
|
|
924
|
+
if (eventIds.length === 0)
|
|
925
|
+
return;
|
|
926
|
+
await storage.transact(async ({ events }) => {
|
|
927
|
+
for (const id of eventIds)
|
|
928
|
+
events.delete(id);
|
|
929
|
+
});
|
|
930
|
+
},
|
|
931
|
+
async health() {
|
|
932
|
+
try {
|
|
933
|
+
return await storage.transact(async ({ meta, events }) => ({
|
|
934
|
+
state: "available",
|
|
935
|
+
queued: await events.count(),
|
|
936
|
+
dropped: numberOr(await meta.get(META_DROPPED), 0)
|
|
937
|
+
}), "readonly");
|
|
938
|
+
} catch {
|
|
939
|
+
return { state: "unavailable", queued: 0, dropped: 0 };
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
// src/tracking/outbox-storage-indexeddb.ts
|
|
945
|
+
var VERSION = 1;
|
|
946
|
+
var EVENTS = "events";
|
|
947
|
+
var META = "meta";
|
|
948
|
+
function requestResult(request) {
|
|
949
|
+
return new Promise((resolve, reject) => {
|
|
950
|
+
request.onsuccess = () => resolve(request.result);
|
|
951
|
+
request.onerror = () => reject(request.error);
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
function transactionDone(transaction) {
|
|
955
|
+
return new Promise((resolve, reject) => {
|
|
956
|
+
transaction.oncomplete = () => resolve();
|
|
957
|
+
const failure = (event) => {
|
|
958
|
+
const target = event.target;
|
|
959
|
+
if (typeof target === "object" && target !== null && "error" in target && target.error instanceof Error) {
|
|
960
|
+
return target.error;
|
|
961
|
+
}
|
|
962
|
+
return transaction.error ?? new Error("IndexedDB transaction aborted");
|
|
963
|
+
};
|
|
964
|
+
transaction.onerror = (event) => reject(failure(event));
|
|
965
|
+
transaction.onabort = (event) => reject(failure(event));
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
function openDatabase(name, factory) {
|
|
969
|
+
return new Promise((resolve, reject) => {
|
|
970
|
+
const request = factory.open(name, VERSION);
|
|
971
|
+
request.onupgradeneeded = () => {
|
|
972
|
+
const database = request.result;
|
|
973
|
+
if (!database.objectStoreNames.contains(EVENTS)) {
|
|
974
|
+
const events = database.createObjectStore(EVENTS, { keyPath: "event.eventId" });
|
|
975
|
+
events.createIndex("sequence", "event.browserSequence", { unique: true });
|
|
976
|
+
}
|
|
977
|
+
if (!database.objectStoreNames.contains(META))
|
|
978
|
+
database.createObjectStore(META);
|
|
979
|
+
};
|
|
980
|
+
request.onsuccess = () => resolve(request.result);
|
|
981
|
+
request.onerror = () => reject(request.error);
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
function indexedDbOutboxStorage(name, factory = () => indexedDB) {
|
|
985
|
+
return {
|
|
986
|
+
async transact(fn, mode = "readwrite") {
|
|
987
|
+
const database = await openDatabase(name, factory());
|
|
988
|
+
try {
|
|
989
|
+
const transaction = database.transaction([META, EVENTS], mode);
|
|
990
|
+
const meta = transaction.objectStore(META);
|
|
991
|
+
const events = transaction.objectStore(EVENTS);
|
|
992
|
+
let result;
|
|
993
|
+
try {
|
|
994
|
+
result = await fn({
|
|
995
|
+
meta: {
|
|
996
|
+
get: (key) => requestResult(meta.get(key)),
|
|
997
|
+
put: (key, value) => void meta.put(value, key),
|
|
998
|
+
delete: (key) => void meta.delete(key)
|
|
999
|
+
},
|
|
1000
|
+
events: {
|
|
1001
|
+
put: (record) => void events.put(record),
|
|
1002
|
+
delete: (eventId) => void events.delete(eventId),
|
|
1003
|
+
all: async () => {
|
|
1004
|
+
const raw = await requestResult(events.index("sequence").getAll());
|
|
1005
|
+
return raw.flatMap((value) => isRecord(value) ? [value] : []);
|
|
1006
|
+
},
|
|
1007
|
+
count: () => requestResult(events.count())
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
transaction.abort();
|
|
1012
|
+
throw error;
|
|
1013
|
+
}
|
|
1014
|
+
await transactionDone(transaction);
|
|
1015
|
+
return result;
|
|
1016
|
+
} finally {
|
|
1017
|
+
database.close();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function isRecord(value) {
|
|
1023
|
+
if (typeof value !== "object" || value === null)
|
|
1024
|
+
return false;
|
|
1025
|
+
const event = Reflect.get(value, "event");
|
|
1026
|
+
return typeof Reflect.get(value, "enqueuedAt") === "number" && typeof event === "object" && event !== null && typeof Reflect.get(event, "eventId") === "string" && typeof Reflect.get(event, "browserSequence") === "number";
|
|
1027
|
+
}
|
|
1028
|
+
// src/tracking/outbox-storage-memory.ts
|
|
1029
|
+
function memoryOutboxStorage() {
|
|
1030
|
+
let meta = new Map;
|
|
1031
|
+
let events = new Map;
|
|
1032
|
+
let chain = Promise.resolve();
|
|
1033
|
+
return {
|
|
1034
|
+
transact(fn) {
|
|
1035
|
+
const run = chain.then(async () => {
|
|
1036
|
+
const nextMeta = new Map(meta);
|
|
1037
|
+
const nextEvents = new Map(events);
|
|
1038
|
+
const result = await fn({
|
|
1039
|
+
meta: {
|
|
1040
|
+
get: async (key) => nextMeta.get(key),
|
|
1041
|
+
put: (key, value) => void nextMeta.set(key, value),
|
|
1042
|
+
delete: (key) => void nextMeta.delete(key)
|
|
1043
|
+
},
|
|
1044
|
+
events: {
|
|
1045
|
+
put: (record) => void nextEvents.set(record.event.eventId, record),
|
|
1046
|
+
delete: (eventId) => void nextEvents.delete(eventId),
|
|
1047
|
+
all: async () => [...nextEvents.values()].sort((a, b) => a.event.browserSequence - b.event.browserSequence),
|
|
1048
|
+
count: async () => nextEvents.size
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
meta = nextMeta;
|
|
1052
|
+
events = nextEvents;
|
|
1053
|
+
return result;
|
|
1054
|
+
});
|
|
1055
|
+
chain = run.catch(() => {
|
|
1056
|
+
return;
|
|
1057
|
+
});
|
|
1058
|
+
return run;
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
export {
|
|
1063
|
+
sendUnloadBeacon,
|
|
1064
|
+
scrollDepthPercent,
|
|
1065
|
+
resolveTrackedClick,
|
|
1066
|
+
resolveAttribution,
|
|
1067
|
+
parseUtmFromSearch,
|
|
1068
|
+
parseReferrer,
|
|
1069
|
+
memoryOutboxStorage,
|
|
1070
|
+
indexedDbOutboxStorage,
|
|
1071
|
+
deliverTrackingBatch,
|
|
1072
|
+
createVisibleTimeMeter,
|
|
1073
|
+
createTrackingSchemas,
|
|
1074
|
+
createTrackingOutbox,
|
|
1075
|
+
createTrackingContract,
|
|
1076
|
+
createTrackingClient,
|
|
1077
|
+
createSequenceReserve,
|
|
1078
|
+
createScrollMilestones,
|
|
1079
|
+
createOncePerPage,
|
|
1080
|
+
browserTrackingHost,
|
|
1081
|
+
VisitBootstrapResponseSchema,
|
|
1082
|
+
UtmDataSchema,
|
|
1083
|
+
TrackingOutboxStateSchema,
|
|
1084
|
+
TrackingDispositionStatusSchema,
|
|
1085
|
+
TrackingDispositionSchema,
|
|
1086
|
+
TrackEventsResponseSchema,
|
|
1087
|
+
DEFAULT_BUILD_ID_PATTERN,
|
|
1088
|
+
CONVENTIONAL_TRACKING_EVENT_TYPES,
|
|
1089
|
+
AttributionDataSchema
|
|
1090
|
+
};
|