@drivemetadata-ai/sdk 0.1.1-beta.2 → 0.1.1-beta.4

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.
@@ -1,9 +1,59 @@
1
+ // src/browser/core/browser-config.ts
2
+ function setIfDefined(target, key, value) {
3
+ if (value !== void 0) {
4
+ target[key] = value;
5
+ }
6
+ }
7
+ function normalizeBrowserConfig(input) {
8
+ const normalized = {
9
+ ...input,
10
+ clientId: input.clientId ?? "",
11
+ workspaceId: input.workspaceId ?? "",
12
+ appId: input.appId ?? ""
13
+ };
14
+ setIfDefined(normalized, "apiHost", input.apiHost);
15
+ setIfDefined(normalized, "capturePageview", input.capturePageview);
16
+ setIfDefined(normalized, "capturePageleave", input.capturePageleave);
17
+ setIfDefined(normalized, "captureDeadClicks", input.captureDeadClicks);
18
+ setIfDefined(normalized, "crossSubdomainCookie", input.crossSubdomainCookie);
19
+ setIfDefined(normalized, "sessionIdleTimeoutSeconds", input.sessionIdleTimeoutSeconds);
20
+ setIfDefined(normalized, "schemaValidation", input.schemaValidation);
21
+ setIfDefined(normalized, "beforeSend", input.beforeSend);
22
+ setIfDefined(normalized, "persistence", input.disablePersistence === true ? "none" : input.persistence);
23
+ return normalized;
24
+ }
25
+
26
+ // src/core/uuid.ts
27
+ function createUuid() {
28
+ const cryptoApi = globalThis.crypto;
29
+ if (typeof cryptoApi?.randomUUID === "function") {
30
+ return cryptoApi.randomUUID();
31
+ }
32
+ const bytes = new Uint8Array(16);
33
+ if (typeof cryptoApi?.getRandomValues === "function") {
34
+ cryptoApi.getRandomValues(bytes);
35
+ } else {
36
+ for (let index = 0; index < bytes.length; index += 1) {
37
+ bytes[index] = Math.floor(Math.random() * 256);
38
+ }
39
+ }
40
+ bytes[6] = (bytes[6] ?? 0) & 15 | 64;
41
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
42
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
43
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
44
+ }
45
+ function isUuid(value) {
46
+ return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
47
+ }
48
+ function ensureUuid(value) {
49
+ return isUuid(value) ? value : createUuid();
50
+ }
51
+
1
52
  // src/core/backend-payload.ts
2
53
  function formatUtcTimestamp(value) {
3
- if (typeof value !== "string") return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
4
- const date = new Date(value);
5
- if (Number.isNaN(date.getTime())) return value;
6
- return date.toISOString().replace("T", " ").slice(0, 19);
54
+ const date = typeof value === "string" || typeof value === "number" || value instanceof Date ? new Date(value) : /* @__PURE__ */ new Date();
55
+ const safeDate = Number.isNaN(date.getTime()) ? /* @__PURE__ */ new Date() : date;
56
+ return safeDate.toISOString().replace("T", " ").slice(0, 19);
7
57
  }
8
58
  function cleanObject(value) {
9
59
  if (Array.isArray(value)) {
@@ -34,15 +84,19 @@ function createBackendCollectorPayload(input) {
34
84
  };
35
85
  const metaData = {
36
86
  ...normalizedEventData,
37
- requestId: input.requestId,
87
+ requestId: ensureUuid(typeof input.requestId === "string" ? input.requestId : void 0),
38
88
  timestamp,
39
89
  eventType: input.eventType,
40
90
  requestFrom: input.requestFrom ?? "3",
41
91
  clientId: input.clientId,
42
92
  workspaceId: input.workspaceId,
43
93
  token: input.token,
44
- anonymousId: eventData.anonymousId ?? input.anonymousId,
45
- sessionId: eventData.sessionId ?? input.sessionId,
94
+ anonymousId: ensureUuid(
95
+ typeof eventData.anonymousId === "string" ? eventData.anonymousId : typeof input.anonymousId === "string" ? input.anonymousId : void 0
96
+ ),
97
+ sessionId: ensureUuid(
98
+ typeof eventData.sessionId === "string" ? eventData.sessionId : typeof input.sessionId === "string" ? input.sessionId : void 0
99
+ ),
46
100
  ua: input.ua,
47
101
  appDetails: { app_id: input.appId },
48
102
  page: { ...page2, url: page2.url ?? input.pageUrl },
@@ -53,12 +107,45 @@ function createBackendCollectorPayload(input) {
53
107
  return cleanObject(payload);
54
108
  }
55
109
 
56
- // src/core/environment.ts
57
- function getBrowserWindow() {
58
- return typeof window === "undefined" ? void 0 : window;
110
+ // src/core/backend-schema.ts
111
+ var requiredMetaDataFields = [
112
+ "requestId",
113
+ "timestamp",
114
+ "eventType",
115
+ "requestFrom",
116
+ "clientId",
117
+ "workspaceId",
118
+ "anonymousId",
119
+ "sessionId"
120
+ ];
121
+ function isPresent(value) {
122
+ return value !== null && value !== void 0 && value !== "";
123
+ }
124
+ function validateBackendCollectorPayload(payload) {
125
+ const errors = [];
126
+ const metaData = payload.metaData;
127
+ if (!metaData || typeof metaData !== "object" || Array.isArray(metaData)) {
128
+ return {
129
+ ok: false,
130
+ errors: ["metaData is required"]
131
+ };
132
+ }
133
+ const metadataRecord = metaData;
134
+ for (const field of requiredMetaDataFields) {
135
+ if (!isPresent(metadataRecord[field])) {
136
+ errors.push(`metaData.${field} is required`);
137
+ }
138
+ }
139
+ if (isPresent(metadataRecord.eventType) && typeof metadataRecord.eventType !== "string") {
140
+ errors.push("metaData.eventType must be a string");
141
+ }
142
+ return {
143
+ ok: errors.length === 0,
144
+ errors
145
+ };
59
146
  }
60
147
 
61
- // src/browser/core/consent.ts
148
+ // src/core/consent.ts
62
149
  var purposes = [
63
150
  "analytics",
64
151
  "advertising",
@@ -98,6 +185,335 @@ function canCollectPurpose(consent2, purpose) {
98
185
  return consent2[purpose] === "granted";
99
186
  }
100
187
 
188
+ // src/core/event-schema.ts
189
+ var reservedKeys = /* @__PURE__ */ new Set(["messageId", "timestamp", "type", "event", "anonymousId", "userId", "context"]);
190
+ function validateEventEnvelope(envelope, options = {}) {
191
+ if (options.mode === "off") return { ok: true, errors: [] };
192
+ const errors = [];
193
+ if (typeof envelope.type !== "string") errors.push("type must be a string");
194
+ if (envelope.type === "track" && typeof envelope.event !== "string") {
195
+ errors.push("track event must include event name");
196
+ }
197
+ if (typeof envelope.messageId !== "string") errors.push("messageId must be a string");
198
+ if (typeof envelope.timestamp !== "string") errors.push("timestamp must be an ISO string");
199
+ for (const key of Object.keys(envelope.properties ?? {})) {
200
+ if (reservedKeys.has(key)) errors.push(`properties.${key} is reserved`);
201
+ }
202
+ return { ok: errors.length === 0, errors };
203
+ }
204
+
205
+ // src/core/environment.ts
206
+ function getBrowserWindow() {
207
+ return typeof window === "undefined" ? void 0 : window;
208
+ }
209
+
210
+ // src/core/privacy.ts
211
+ var sensitiveKeys = /* @__PURE__ */ new Set([
212
+ "email",
213
+ "phone",
214
+ "mobile",
215
+ "address",
216
+ "address1",
217
+ "address2",
218
+ "first_name",
219
+ "last_name",
220
+ "name",
221
+ "token",
222
+ "secret",
223
+ "password",
224
+ "session",
225
+ "cookie"
226
+ ]);
227
+ function sanitizeValue(value, allow, path = []) {
228
+ if (Array.isArray(value)) {
229
+ return value.map((item) => sanitizeValue(item, allow, path));
230
+ }
231
+ if (value && typeof value === "object") {
232
+ return Object.fromEntries(
233
+ Object.entries(value).filter(([key]) => {
234
+ const lowerKey = key.toLowerCase();
235
+ const lowerPath = path.map((item) => item.toLowerCase());
236
+ const isEcommerceItemName = lowerKey === "name" && lowerPath.includes("ecommerce") && lowerPath.includes("items");
237
+ return isEcommerceItemName || !sensitiveKeys.has(lowerKey) || allow.has(lowerKey);
238
+ }).map(([key, nestedValue]) => [key, sanitizeValue(nestedValue, allow, [...path, key])])
239
+ );
240
+ }
241
+ return value;
242
+ }
243
+ function sanitizeProperties(input, allowRawKeys = []) {
244
+ const allow = new Set(allowRawKeys.map((key) => key.toLowerCase()));
245
+ return sanitizeValue(input, allow);
246
+ }
247
+
248
+ // src/core/attribution.ts
249
+ var dmdUtmKeys = [
250
+ "utm_source",
251
+ "utm_medium",
252
+ "utm_campaign",
253
+ "utm_term",
254
+ "utm_content",
255
+ "utm_id"
256
+ ];
257
+ var dmdCampaignKeys = ["campaign_id", "ad_id"];
258
+ var dmdClickIdSources = [
259
+ ["gclid", 2],
260
+ ["fbclid", 3],
261
+ ["ScCid", 1],
262
+ ["li_fat_id", 4]
263
+ ];
264
+ function cleanAttributionRecord(record) {
265
+ const cleaned = Object.fromEntries(
266
+ Object.entries(record).filter(([, value]) => value !== null && value !== void 0 && value !== "")
267
+ );
268
+ return Object.keys(cleaned).length > 0 ? cleaned : void 0;
269
+ }
270
+ function objectValue(value) {
271
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
272
+ }
273
+ function mergeAttributionRecords(explicitProperties, stored) {
274
+ const attributionData = objectValue(explicitProperties.attributionData);
275
+ const utmParameter = objectValue(explicitProperties.utmParameter);
276
+ return {
277
+ ...explicitProperties,
278
+ ...stored.attributionData || attributionData ? { attributionData: { ...stored.attributionData ?? {}, ...attributionData ?? {} } } : {},
279
+ ...stored.utmParameter || utmParameter ? { utmParameter: { ...stored.utmParameter ?? {}, ...utmParameter ?? {} } } : {}
280
+ };
281
+ }
282
+
283
+ // src/browser/core/attribution.ts
284
+ function getCurrentUrl() {
285
+ return getBrowserWindow()?.location?.href;
286
+ }
287
+ function getCookie(name) {
288
+ const cookie = getBrowserWindow()?.document?.cookie;
289
+ if (!cookie) return void 0;
290
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
291
+ const match = new RegExp(`(?:^|; )${escapedName}=([^;]*)`).exec(cookie);
292
+ return match ? decodeURIComponent(match[1] ?? "") : void 0;
293
+ }
294
+ function getParams(url) {
295
+ return new URL(url, "https://placeholder.local").searchParams;
296
+ }
297
+ function setIfPresent(persistence, key, value) {
298
+ if (value !== null && value !== "") {
299
+ persistence.setItem(key, value);
300
+ }
301
+ }
302
+ function getStoredString(persistence, key) {
303
+ const value = persistence.getItem(key);
304
+ return value === null || value === "" ? void 0 : value;
305
+ }
306
+ function getStoredNumberOrString(persistence, key) {
307
+ const value = getStoredString(persistence, key);
308
+ if (value === void 0) return void 0;
309
+ const parsed = Number.parseInt(value, 10);
310
+ return Number.isNaN(parsed) ? value : parsed;
311
+ }
312
+ function captureAttributionFromUrl(persistence, url = getCurrentUrl()) {
313
+ if (!url) return;
314
+ const params = getParams(url);
315
+ for (const key of dmdUtmKeys) {
316
+ setIfPresent(persistence, key, params.get(key));
317
+ }
318
+ for (const key of dmdCampaignKeys) {
319
+ setIfPresent(persistence, key, params.get(key));
320
+ }
321
+ for (const [param, sdkPubId] of dmdClickIdSources) {
322
+ const value = params.get(param);
323
+ if (value) {
324
+ persistence.setItem("unique_id", value);
325
+ persistence.setItem("sdk_pub_id", String(sdkPubId));
326
+ }
327
+ }
328
+ }
329
+ function getStoredAttributionData(persistence) {
330
+ return cleanAttributionRecord({
331
+ unique_id: getStoredString(persistence, "unique_id"),
332
+ sdk_pub_id: getStoredNumberOrString(persistence, "sdk_pub_id"),
333
+ fbc: getStoredString(persistence, "fbc") ?? getCookie("_fbc"),
334
+ fbp: getStoredString(persistence, "fbp") ?? getCookie("_fbp"),
335
+ campaign_id: getStoredString(persistence, "campaign_id"),
336
+ ad_id: getStoredString(persistence, "ad_id")
337
+ });
338
+ }
339
+ function getStoredUtmParameter(persistence) {
340
+ return cleanAttributionRecord(Object.fromEntries(dmdUtmKeys.map((key) => [key, getStoredString(persistence, key)])));
341
+ }
342
+ function mergeStoredAttribution(properties, persistence) {
343
+ const stored = {};
344
+ const attributionData = getStoredAttributionData(persistence);
345
+ const utmParameter = getStoredUtmParameter(persistence);
346
+ if (attributionData !== void 0) stored.attributionData = attributionData;
347
+ if (utmParameter !== void 0) stored.utmParameter = utmParameter;
348
+ return mergeAttributionRecords(properties, stored);
349
+ }
350
+
351
+ // src/browser/core/autocapture.ts
352
+ function installAutocapture(config) {
353
+ const trackedPageUrls = /* @__PURE__ */ new Set();
354
+ const timers = /* @__PURE__ */ new Set();
355
+ const pageviewDelayMs = config.pageviewDelayMs ?? 500;
356
+ const history = config.browserWindow.history;
357
+ const originalPushState = history?.pushState;
358
+ const originalReplaceState = history?.replaceState;
359
+ function clearTimer(timer) {
360
+ timers.delete(timer);
361
+ clearTimeout(timer);
362
+ }
363
+ function canonicalUrl() {
364
+ return config.browserWindow.location.href;
365
+ }
366
+ function trackCurrentPageview() {
367
+ if (!config.capturePageview) return;
368
+ const url = canonicalUrl();
369
+ if (trackedPageUrls.has(url)) return;
370
+ trackedPageUrls.add(url);
371
+ config.onPageView();
372
+ }
373
+ function schedulePageview(delayMs) {
374
+ if (!config.capturePageview) return;
375
+ const timer = setTimeout(() => {
376
+ timers.delete(timer);
377
+ trackCurrentPageview();
378
+ }, delayMs);
379
+ timers.add(timer);
380
+ }
381
+ function handleRouteChange() {
382
+ config.onRouteChange?.();
383
+ trackCurrentPageview();
384
+ }
385
+ function wrapHistoryMethod(method) {
386
+ if (!history) return;
387
+ const original = history[method];
388
+ if (typeof original !== "function") return;
389
+ history[method] = function wrappedHistoryMethod(...args) {
390
+ const result = original.apply(this, args);
391
+ const timer = setTimeout(() => {
392
+ timers.delete(timer);
393
+ handleRouteChange();
394
+ }, 0);
395
+ timers.add(timer);
396
+ return result;
397
+ };
398
+ }
399
+ function handlePopOrHashChange() {
400
+ handleRouteChange();
401
+ }
402
+ function handlePageLeave() {
403
+ if (config.capturePageleave) {
404
+ config.onPageLeave();
405
+ }
406
+ }
407
+ schedulePageview(pageviewDelayMs);
408
+ wrapHistoryMethod("pushState");
409
+ wrapHistoryMethod("replaceState");
410
+ const canListen = typeof config.browserWindow.addEventListener === "function" && typeof config.browserWindow.removeEventListener === "function";
411
+ if (canListen) {
412
+ config.browserWindow.addEventListener("popstate", handlePopOrHashChange);
413
+ config.browserWindow.addEventListener("hashchange", handlePopOrHashChange);
414
+ config.browserWindow.addEventListener("beforeunload", handlePageLeave);
415
+ }
416
+ return {
417
+ cleanup() {
418
+ for (const timer of Array.from(timers)) {
419
+ clearTimer(timer);
420
+ }
421
+ if (history && originalPushState) history.pushState = originalPushState;
422
+ if (history && originalReplaceState) history.replaceState = originalReplaceState;
423
+ if (canListen) {
424
+ config.browserWindow.removeEventListener("popstate", handlePopOrHashChange);
425
+ config.browserWindow.removeEventListener("hashchange", handlePopOrHashChange);
426
+ config.browserWindow.removeEventListener("beforeunload", handlePageLeave);
427
+ }
428
+ }
429
+ };
430
+ }
431
+
432
+ // src/core/payload-size.ts
433
+ var preserveStringKeys = /* @__PURE__ */ new Set([
434
+ "requestId",
435
+ "eventType",
436
+ "requestFrom",
437
+ "clientId",
438
+ "workspaceId",
439
+ "anonymousId",
440
+ "sessionId",
441
+ "token"
442
+ ]);
443
+ function payloadByteLength(payload) {
444
+ const serialized = JSON.stringify(payload);
445
+ if (typeof TextEncoder !== "undefined") {
446
+ return new TextEncoder().encode(serialized).length;
447
+ }
448
+ return serialized.length;
449
+ }
450
+ function clonePayload(payload) {
451
+ try {
452
+ return JSON.parse(JSON.stringify(payload));
453
+ } catch {
454
+ return void 0;
455
+ }
456
+ }
457
+ function truncateValue(value, truncateStringLength, key) {
458
+ if (typeof value === "string") {
459
+ if (key && preserveStringKeys.has(key)) return value;
460
+ if (value.length <= truncateStringLength) return value;
461
+ return `${value.slice(0, truncateStringLength)}...[TRUNCATED]`;
462
+ }
463
+ if (Array.isArray(value)) {
464
+ return value.map((item) => truncateValue(item, truncateStringLength));
465
+ }
466
+ if (value && typeof value === "object") {
467
+ return Object.fromEntries(
468
+ Object.entries(value).map(([childKey, childValue]) => [
469
+ childKey,
470
+ truncateValue(childValue, truncateStringLength, childKey)
471
+ ])
472
+ );
473
+ }
474
+ return value;
475
+ }
476
+ function markPayloadTruncated(payload) {
477
+ if (payload.metaData && typeof payload.metaData === "object" && !Array.isArray(payload.metaData)) {
478
+ return {
479
+ ...payload,
480
+ metaData: {
481
+ ...payload.metaData,
482
+ payloadTruncated: true
483
+ }
484
+ };
485
+ }
486
+ return {
487
+ ...payload,
488
+ payloadTruncated: true
489
+ };
490
+ }
491
+ function truncatePayload(payload, truncateStringLength) {
492
+ const cloned = clonePayload(payload);
493
+ if (!cloned) return void 0;
494
+ return markPayloadTruncated(truncateValue(cloned, truncateStringLength));
495
+ }
496
+ function getPayloadMessageId(payload) {
497
+ const value = payload.metaData?.requestId ?? payload.messageId;
498
+ return value === void 0 ? void 0 : String(value);
499
+ }
500
+ function preparePayloadForSizePolicy(payload, options = {}) {
501
+ const maxPayloadBytes = options.maxPayloadBytes ?? 64e3;
502
+ const payloadSizePolicy = options.payloadSizePolicy ?? "drop";
503
+ const payloadTruncateStringLength = options.payloadTruncateStringLength ?? 1024;
504
+ if (payloadByteLength(payload) <= maxPayloadBytes) {
505
+ return { ok: true, payload, truncated: false };
506
+ }
507
+ if (payloadSizePolicy === "truncate") {
508
+ const truncatedPayload = truncatePayload(payload, payloadTruncateStringLength);
509
+ if (truncatedPayload && payloadByteLength(truncatedPayload) <= maxPayloadBytes) {
510
+ return { ok: true, payload: truncatedPayload, truncated: true };
511
+ }
512
+ }
513
+ const messageId = getPayloadMessageId(payload);
514
+ return messageId === void 0 ? { ok: false, reason: "payload_too_large" } : { ok: false, reason: "payload_too_large", messageId };
515
+ }
516
+
101
517
  // src/browser/core/delivery.ts
102
518
  function createId(prefix) {
103
519
  return `${prefix}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
@@ -129,7 +545,6 @@ function createDeliveryManager(config) {
129
545
  const lockTtlMs = config.lockTtlMs ?? 5e3;
130
546
  const tabId = config.tabId ?? createId("tab");
131
547
  const batchSize = config.batchSize ?? 25;
132
- const maxPayloadBytes = config.maxPayloadBytes ?? 64e3;
133
548
  function recordDrop(event) {
134
549
  diagnostics.dropped.push(event);
135
550
  config.onDrop?.(event);
@@ -138,12 +553,22 @@ function createDeliveryManager(config) {
138
553
  diagnostics.lastError = error.message;
139
554
  config.onError?.(error);
140
555
  }
141
- function payloadByteLength(payload) {
142
- const serialized = JSON.stringify(payload);
143
- if (typeof TextEncoder !== "undefined") {
144
- return new TextEncoder().encode(serialized).length;
556
+ function preparePayloadForSend(payload) {
557
+ const sizePolicyOptions = {};
558
+ if (config.maxPayloadBytes !== void 0) sizePolicyOptions.maxPayloadBytes = config.maxPayloadBytes;
559
+ if (config.payloadSizePolicy !== void 0) sizePolicyOptions.payloadSizePolicy = config.payloadSizePolicy;
560
+ if (config.payloadTruncateStringLength !== void 0) {
561
+ sizePolicyOptions.payloadTruncateStringLength = config.payloadTruncateStringLength;
145
562
  }
146
- return serialized.length;
563
+ const prepared = preparePayloadForSizePolicy(payload, sizePolicyOptions);
564
+ if (prepared.ok) return prepared.payload;
565
+ const diagnostic = {
566
+ reason: prepared.reason,
567
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
568
+ };
569
+ if (prepared.messageId !== void 0) diagnostic.messageId = prepared.messageId;
570
+ recordDrop(diagnostic);
571
+ return void 0;
147
572
  }
148
573
  function recordStorageUnavailable() {
149
574
  recordDrop({
@@ -233,7 +658,19 @@ function createDeliveryManager(config) {
233
658
  idempotencyKey: String(payload.idempotencyKey ?? createIdempotencyKey(payload, messageId))
234
659
  };
235
660
  }
661
+ function tryBeacon(body) {
662
+ if (!config.useBeacon) return false;
663
+ const sendBeacon = globalThis.navigator?.sendBeacon;
664
+ if (typeof sendBeacon !== "function") return false;
665
+ try {
666
+ const blob = new Blob([JSON.stringify(body)], { type: "application/json" });
667
+ return sendBeacon.call(globalThis.navigator, config.endpoint, blob);
668
+ } catch {
669
+ return false;
670
+ }
671
+ }
236
672
  async function deliver(body) {
673
+ if (tryBeacon(body)) return;
237
674
  const fetchImpl = config.fetch ?? globalThis.fetch;
238
675
  if (typeof fetchImpl !== "function") {
239
676
  throw new Error("fetch_unavailable");
@@ -250,26 +687,22 @@ function createDeliveryManager(config) {
250
687
  return {
251
688
  async send(payload) {
252
689
  const body = withEnvelope(payload);
253
- if (payloadByteLength(body) > maxPayloadBytes) {
254
- recordDrop({
255
- messageId: String(body.metaData?.requestId ?? body.messageId),
256
- reason: "payload_too_large",
257
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
258
- });
690
+ const preparedBody = preparePayloadForSend(body);
691
+ if (!preparedBody) {
259
692
  return;
260
693
  }
261
694
  diagnostics.inFlight += 1;
262
695
  try {
263
- await deliver(body);
696
+ await deliver(preparedBody);
264
697
  } catch (error) {
265
698
  const deliveryError = error instanceof Error ? error : new Error(String(error));
266
699
  recordError(deliveryError);
267
700
  enqueue({
268
- messageId: String(body.metaData?.requestId ?? body.messageId),
701
+ messageId: String(getPayloadMessageId(body)),
269
702
  savedAt: Date.now(),
270
703
  attempts: 1,
271
704
  lastError: deliveryError.message,
272
- payload: body
705
+ payload: preparedBody
273
706
  });
274
707
  } finally {
275
708
  diagnostics.inFlight -= 1;
@@ -382,60 +815,268 @@ function createDeliveryManager(config) {
382
815
  };
383
816
  }
384
817
 
385
- // src/browser/core/privacy.ts
386
- var sensitiveKeys = /* @__PURE__ */ new Set([
387
- "email",
388
- "phone",
389
- "mobile",
390
- "address",
391
- "address1",
392
- "address2",
393
- "first_name",
394
- "last_name",
395
- "name",
396
- "token",
397
- "secret",
398
- "password",
399
- "session",
400
- "cookie"
401
- ]);
402
- function sanitizeValue(value, allow) {
403
- if (Array.isArray(value)) {
404
- return value.map((item) => sanitizeValue(item, allow));
818
+ // src/browser/core/identity.ts
819
+ var ANONYMOUS_ID_STORAGE_KEY = "dmd_anonymous_id";
820
+ var LEGACY_ANONYMOUS_ID_STORAGE_KEY = "anonymousId";
821
+ function getUrlParam(name) {
822
+ const href = getBrowserWindow()?.location?.href;
823
+ if (!href) return null;
824
+ try {
825
+ return new URL(href).searchParams.get(name);
826
+ } catch {
827
+ return null;
405
828
  }
406
- if (value && typeof value === "object") {
407
- return Object.fromEntries(
408
- Object.entries(value).filter(([key]) => !sensitiveKeys.has(key.toLowerCase()) || allow.has(key.toLowerCase())).map(([key, nestedValue]) => [key, sanitizeValue(nestedValue, allow)])
409
- );
829
+ }
830
+ function resolveAnonymousId(persistence) {
831
+ const urlAnonymousId = getUrlParam("aid");
832
+ if (isUuid(urlAnonymousId)) {
833
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, urlAnonymousId);
834
+ return urlAnonymousId;
410
835
  }
411
- return value;
836
+ const storedAnonymousId = persistence.getItem(ANONYMOUS_ID_STORAGE_KEY);
837
+ if (isUuid(storedAnonymousId)) {
838
+ return storedAnonymousId;
839
+ }
840
+ const legacyAnonymousId = persistence.getItem(LEGACY_ANONYMOUS_ID_STORAGE_KEY);
841
+ if (isUuid(legacyAnonymousId)) {
842
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, legacyAnonymousId);
843
+ return legacyAnonymousId;
844
+ }
845
+ const anonymousId = createUuid();
846
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, anonymousId);
847
+ return anonymousId;
412
848
  }
413
- function sanitizeProperties(input, allowRawKeys = []) {
414
- const allow = new Set(allowRawKeys.map((key) => key.toLowerCase()));
415
- return sanitizeValue(input, allow);
849
+ function resetAnonymousId(persistence) {
850
+ const anonymousId = createUuid();
851
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, anonymousId);
852
+ return anonymousId;
416
853
  }
417
854
 
418
- // src/browser/core/schema.ts
419
- var reservedKeys = /* @__PURE__ */ new Set(["messageId", "timestamp", "type", "event", "anonymousId", "userId", "context"]);
420
- function validateEventEnvelope(envelope, options = {}) {
421
- if (options.mode === "off") return { ok: true, errors: [] };
422
- const errors = [];
423
- if (typeof envelope.type !== "string") errors.push("type must be a string");
424
- if (envelope.type === "track" && typeof envelope.event !== "string") {
425
- errors.push("track event must include event name");
855
+ // src/browser/core/persistence.ts
856
+ function createPersistenceHealth(requested) {
857
+ return {
858
+ requested,
859
+ cookieFallbackUsed: false,
860
+ memoryFallbackUsed: requested === "memory",
861
+ failures: []
862
+ };
863
+ }
864
+ function recordFailure(health, backend, operation, error) {
865
+ health.failures.push({
866
+ backend,
867
+ operation,
868
+ message: error instanceof Error ? error.message : String(error)
869
+ });
870
+ if (health.failures.length > 25) {
871
+ health.failures.shift();
426
872
  }
427
- if (typeof envelope.messageId !== "string") errors.push("messageId must be a string");
428
- if (typeof envelope.timestamp !== "string") errors.push("timestamp must be an ISO string");
429
- for (const key of Object.keys(envelope.properties ?? {})) {
430
- if (reservedKeys.has(key)) errors.push(`properties.${key} is reserved`);
873
+ }
874
+ function createMemoryPersistence(health = createPersistenceHealth("memory")) {
875
+ const memory = {};
876
+ return {
877
+ getItem(key) {
878
+ return Object.prototype.hasOwnProperty.call(memory, key) ? memory[key] ?? null : null;
879
+ },
880
+ setItem(key, value) {
881
+ memory[key] = value;
882
+ return true;
883
+ },
884
+ removeItem(key) {
885
+ delete memory[key];
886
+ },
887
+ getHealth() {
888
+ return {
889
+ ...health,
890
+ failures: health.failures.map((failure) => ({ ...failure }))
891
+ };
892
+ }
893
+ };
894
+ }
895
+ function getCookie2(name) {
896
+ const browserWindow = getBrowserWindow();
897
+ const cookie = browserWindow?.document?.cookie;
898
+ if (!cookie) return null;
899
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
900
+ const match = new RegExp(`(?:^|; )${escapedName}=([^;]*)`).exec(cookie);
901
+ return match ? decodeURIComponent(match[1] ?? "") : null;
902
+ }
903
+ function setCookie(name, value) {
904
+ const browserWindow = getBrowserWindow();
905
+ if (!browserWindow?.document) return false;
906
+ try {
907
+ const expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3).toUTCString();
908
+ const secure = browserWindow.location?.protocol === "https:" ? "; Secure" : "";
909
+ browserWindow.document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/${secure}; SameSite=Lax`;
910
+ return true;
911
+ } catch {
912
+ return false;
913
+ }
914
+ }
915
+ function removeCookie(name) {
916
+ const browserWindow = getBrowserWindow();
917
+ if (!browserWindow?.document) return;
918
+ browserWindow.document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
919
+ }
920
+ function getLocalStorage() {
921
+ const browserWindow = getBrowserWindow();
922
+ try {
923
+ return browserWindow?.localStorage;
924
+ } catch {
925
+ return void 0;
926
+ }
927
+ }
928
+ function getSessionStorage() {
929
+ const browserWindow = getBrowserWindow();
930
+ try {
931
+ return browserWindow?.sessionStorage;
932
+ } catch {
933
+ return void 0;
431
934
  }
432
- return { ok: errors.length === 0, errors };
935
+ }
936
+ function createBrowserPersistence(mode = "localStorage+cookie") {
937
+ const health = createPersistenceHealth(mode);
938
+ const memory = createMemoryPersistence(health);
939
+ if (mode === "none") {
940
+ return {
941
+ getItem() {
942
+ return null;
943
+ },
944
+ setItem() {
945
+ return false;
946
+ },
947
+ removeItem() {
948
+ },
949
+ getHealth() {
950
+ return {
951
+ ...health,
952
+ failures: health.failures.map((failure) => ({ ...failure }))
953
+ };
954
+ }
955
+ };
956
+ }
957
+ if (mode === "memory") return memory;
958
+ const primary = mode === "sessionStorage" ? getSessionStorage() : getLocalStorage();
959
+ const useCookie = mode === "cookie" || mode === "localStorage+cookie" || !primary;
960
+ return {
961
+ getItem(key) {
962
+ try {
963
+ const stored = primary?.getItem(key);
964
+ if (stored) return stored;
965
+ } catch (error) {
966
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "get", error);
967
+ }
968
+ if (useCookie) {
969
+ const cookie = getCookie2(key);
970
+ if (cookie) {
971
+ health.cookieFallbackUsed = true;
972
+ return cookie;
973
+ }
974
+ }
975
+ const value = memory.getItem(key);
976
+ if (value !== null) health.memoryFallbackUsed = true;
977
+ return value;
978
+ },
979
+ setItem(key, value) {
980
+ let wrote = false;
981
+ try {
982
+ primary?.setItem(key, value);
983
+ wrote = primary !== void 0;
984
+ } catch (error) {
985
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "set", error);
986
+ wrote = false;
987
+ }
988
+ if (useCookie) {
989
+ const cookieWrote = setCookie(key, value);
990
+ if (cookieWrote) health.cookieFallbackUsed = true;
991
+ wrote = cookieWrote || wrote;
992
+ }
993
+ if (!wrote) {
994
+ health.memoryFallbackUsed = true;
995
+ return memory.setItem(key, value);
996
+ }
997
+ if (mode === "localStorage+cookie") {
998
+ memory.setItem(key, value);
999
+ }
1000
+ return true;
1001
+ },
1002
+ removeItem(key) {
1003
+ try {
1004
+ primary?.removeItem(key);
1005
+ } catch (error) {
1006
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "remove", error);
1007
+ }
1008
+ if (useCookie) removeCookie(key);
1009
+ memory.removeItem(key);
1010
+ },
1011
+ getHealth() {
1012
+ return {
1013
+ ...health,
1014
+ failures: health.failures.map((failure) => ({ ...failure }))
1015
+ };
1016
+ }
1017
+ };
433
1018
  }
434
1019
 
435
- // src/browser/core/DriveMetaDataSDK.ts
436
- function createId2(prefix) {
437
- return `${prefix}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
1020
+ // src/browser/core/session.ts
1021
+ var SESSION_STORAGE_KEY = "sessionData";
1022
+ function getUrlParam2(name) {
1023
+ const href = getBrowserWindow()?.location?.href;
1024
+ if (!href) return null;
1025
+ try {
1026
+ return new URL(href).searchParams.get(name);
1027
+ } catch {
1028
+ return null;
1029
+ }
438
1030
  }
1031
+ function readStoredSession(persistence) {
1032
+ const rawSession = persistence.getItem(SESSION_STORAGE_KEY);
1033
+ if (!rawSession) return void 0;
1034
+ try {
1035
+ const parsed = JSON.parse(rawSession);
1036
+ if (isUuid(parsed.sessionId) && typeof parsed.timestamp === "number") {
1037
+ return {
1038
+ sessionId: parsed.sessionId,
1039
+ timestamp: parsed.timestamp
1040
+ };
1041
+ }
1042
+ } catch {
1043
+ persistence.removeItem(SESSION_STORAGE_KEY);
1044
+ }
1045
+ return void 0;
1046
+ }
1047
+ function writeStoredSession(persistence, sessionId, timestamp) {
1048
+ persistence.setItem(SESSION_STORAGE_KEY, JSON.stringify({ sessionId, timestamp }));
1049
+ }
1050
+ function createSessionManager(persistence, idleTimeoutSeconds = 30 * 60) {
1051
+ function resolveSessionId() {
1052
+ const now = Date.now();
1053
+ const urlSessionId = getUrlParam2("sid") ?? getUrlParam2("session_id");
1054
+ if (isUuid(urlSessionId)) {
1055
+ writeStoredSession(persistence, urlSessionId, now);
1056
+ return urlSessionId;
1057
+ }
1058
+ const storedSession = readStoredSession(persistence);
1059
+ if (storedSession && now - storedSession.timestamp <= idleTimeoutSeconds * 1e3) {
1060
+ writeStoredSession(persistence, storedSession.sessionId, now);
1061
+ return storedSession.sessionId;
1062
+ }
1063
+ const sessionId = createUuid();
1064
+ writeStoredSession(persistence, sessionId, now);
1065
+ return sessionId;
1066
+ }
1067
+ return {
1068
+ getSessionId() {
1069
+ return resolveSessionId();
1070
+ },
1071
+ reset() {
1072
+ const sessionId = createUuid();
1073
+ writeStoredSession(persistence, sessionId, Date.now());
1074
+ return sessionId;
1075
+ }
1076
+ };
1077
+ }
1078
+
1079
+ // src/browser/core/DriveMetaDataSDK.ts
439
1080
  function endpointFromConfig(config) {
440
1081
  const host = config.apiHost ?? "https://sdk.drivemetadata.com/v2";
441
1082
  return `${host.replace(/\/$/, "")}/data-collector`;
@@ -446,14 +1087,6 @@ function requireConfigString(value, field) {
446
1087
  }
447
1088
  return value;
448
1089
  }
449
- function getBrowserStorage() {
450
- const browserWindow = getBrowserWindow();
451
- try {
452
- return browserWindow?.localStorage;
453
- } catch {
454
- return void 0;
455
- }
456
- }
457
1090
  var DriveMetaDataSDK = class {
458
1091
  constructor(config) {
459
1092
  this.initialized = true;
@@ -466,30 +1099,35 @@ var DriveMetaDataSDK = class {
466
1099
  requireConfigString(config.writeKey || config.token, "writeKey or token");
467
1100
  this.config = config;
468
1101
  this.endpoint = endpointFromConfig(config);
469
- const storage = getBrowserStorage();
1102
+ this.persistence = createBrowserPersistence(config.persistence);
1103
+ this.session = createSessionManager(this.persistence, config.sessionIdleTimeoutSeconds);
470
1104
  const deliveryConfig = {
471
- endpoint: this.endpoint
1105
+ endpoint: this.endpoint,
1106
+ storage: this.persistence
472
1107
  };
473
1108
  if (config.delivery?.maxQueueSize !== void 0) deliveryConfig.maxQueueSize = config.delivery.maxQueueSize;
474
1109
  if (config.delivery?.queueTtlMs !== void 0) deliveryConfig.queueTtlMs = config.delivery.queueTtlMs;
475
1110
  if (config.delivery?.retryDelayMs !== void 0) deliveryConfig.retryDelayMs = config.delivery.retryDelayMs;
476
1111
  if (config.delivery?.maxRetryDelayMs !== void 0) deliveryConfig.maxRetryDelayMs = config.delivery.maxRetryDelayMs;
477
1112
  if (config.delivery?.maxPayloadBytes !== void 0) deliveryConfig.maxPayloadBytes = config.delivery.maxPayloadBytes;
1113
+ if (config.delivery?.payloadSizePolicy !== void 0) deliveryConfig.payloadSizePolicy = config.delivery.payloadSizePolicy;
1114
+ if (config.delivery?.payloadTruncateStringLength !== void 0) {
1115
+ deliveryConfig.payloadTruncateStringLength = config.delivery.payloadTruncateStringLength;
1116
+ }
1117
+ if (config.delivery?.useBeacon !== void 0) deliveryConfig.useBeacon = config.delivery.useBeacon;
478
1118
  if (config.delivery?.batchSize !== void 0) deliveryConfig.batchSize = config.delivery.batchSize;
479
1119
  if (config.onDrop !== void 0) deliveryConfig.onDrop = config.onDrop;
480
1120
  if (config.onError !== void 0) deliveryConfig.onError = config.onError;
481
- this.delivery = createDeliveryManager(storage ? {
482
- ...deliveryConfig,
483
- storage
484
- } : deliveryConfig);
1121
+ this.delivery = createDeliveryManager(deliveryConfig);
485
1122
  this.initialRetryDelayMs = config.delivery?.retryDelayMs ?? 1e3;
486
1123
  this.retryDelayMs = this.initialRetryDelayMs;
487
1124
  this.maxRetryDelayMs = config.delivery?.maxRetryDelayMs ?? 3e4;
488
1125
  this.writeKey = config.writeKey || config.token || "";
489
- this.identity = { anonymousId: createId2("anon") };
490
- this.sessionId = createId2("session");
1126
+ this.identity = { anonymousId: resolveAnonymousId(this.persistence) };
1127
+ captureAttributionFromUrl(this.persistence);
491
1128
  this.consentState = normalizeConsent(config.gdprConsent ?? config.consent);
492
1129
  this.gdprConsent = this.consentState.analytics;
1130
+ this.installAutocapture();
493
1131
  if (!config.delivery?.disableLifecycleFlush) {
494
1132
  this.installLifecycleFlush();
495
1133
  }
@@ -529,7 +1167,8 @@ var DriveMetaDataSDK = class {
529
1167
  }
530
1168
  }
531
1169
  reset() {
532
- this.identity = { anonymousId: createId2("anon") };
1170
+ this.identity = { anonymousId: resetAnonymousId(this.persistence) };
1171
+ this.session.reset();
533
1172
  this.queue = [];
534
1173
  this.offline = false;
535
1174
  if (this.retryTimer !== void 0) {
@@ -538,8 +1177,20 @@ var DriveMetaDataSDK = class {
538
1177
  }
539
1178
  this.lifecycleCleanup?.();
540
1179
  this.lifecycleCleanup = void 0;
1180
+ this.autocaptureCleanup?.();
1181
+ this.autocaptureCleanup = void 0;
541
1182
  this.delivery.clearQueue("manual_clear");
542
1183
  }
1184
+ disposeForTests() {
1185
+ if (this.retryTimer !== void 0) {
1186
+ clearTimeout(this.retryTimer);
1187
+ this.retryTimer = void 0;
1188
+ }
1189
+ this.lifecycleCleanup?.();
1190
+ this.lifecycleCleanup = void 0;
1191
+ this.autocaptureCleanup?.();
1192
+ this.autocaptureCleanup = void 0;
1193
+ }
543
1194
  setConsent(consent2) {
544
1195
  this.consentState = typeof consent2 === "object" ? mergeConsent(this.consentState, consent2) : normalizeConsent(consent2);
545
1196
  this.gdprConsent = this.consentState.analytics;
@@ -554,6 +1205,7 @@ var DriveMetaDataSDK = class {
554
1205
  initialized: this.initialized,
555
1206
  consent: this.gdprConsent,
556
1207
  consentPurposes: this.consentState,
1208
+ persistence: this.persistence.getHealth(),
557
1209
  queueSize: deliveryDiagnostics.queued,
558
1210
  offline: this.offline || deliveryDiagnostics.queued > 0,
559
1211
  droppedEvents: this.droppedEvents + deliveryDiagnostics.dropped.length,
@@ -568,7 +1220,9 @@ var DriveMetaDataSDK = class {
568
1220
  void this.delivery.send(payload).then(() => {
569
1221
  const diagnostics = this.delivery.getDiagnostics();
570
1222
  this.offline = diagnostics.queued > 0;
571
- this.lastError = diagnostics.lastError;
1223
+ if (diagnostics.lastError !== void 0) {
1224
+ this.lastError = diagnostics.lastError;
1225
+ }
572
1226
  if (diagnostics.queued > 0) {
573
1227
  this.scheduleRetryFlush();
574
1228
  }
@@ -600,6 +1254,28 @@ var DriveMetaDataSDK = class {
600
1254
  }
601
1255
  };
602
1256
  }
1257
+ installAutocapture() {
1258
+ const browserWindow = getBrowserWindow();
1259
+ if (!browserWindow) return;
1260
+ if (this.config.autocapture === false) return;
1261
+ const capturePageview = this.config.capturePageview !== false;
1262
+ const capturePageleave = this.config.capturePageleave !== false;
1263
+ const controller = installAutocapture({
1264
+ browserWindow,
1265
+ capturePageview,
1266
+ capturePageleave,
1267
+ onPageView: () => {
1268
+ this.page();
1269
+ },
1270
+ onPageLeave: () => {
1271
+ this.trackEvent("page_leave", { url: browserWindow.location.href });
1272
+ },
1273
+ onRouteChange: () => {
1274
+ captureAttributionFromUrl(this.persistence);
1275
+ }
1276
+ });
1277
+ this.autocaptureCleanup = controller.cleanup;
1278
+ }
603
1279
  scheduleRetryFlush() {
604
1280
  if (this.retryTimer !== void 0) return;
605
1281
  const delay = Math.min(this.retryDelayMs, this.maxRetryDelayMs);
@@ -625,7 +1301,7 @@ var DriveMetaDataSDK = class {
625
1301
  type,
626
1302
  event,
627
1303
  properties: sanitizeProperties(properties),
628
- messageId: options.messageId ?? createId2("msg"),
1304
+ messageId: ensureUuid(options.messageId),
629
1305
  timestamp: options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
630
1306
  context: options.context ?? {},
631
1307
  anonymousId: this.identity.anonymousId,
@@ -634,7 +1310,7 @@ var DriveMetaDataSDK = class {
634
1310
  appId: this.config.appId,
635
1311
  writeKey: this.writeKey,
636
1312
  consent: this.consentState,
637
- sessionId: this.sessionId
1313
+ sessionId: this.session.getSessionId()
638
1314
  };
639
1315
  if (this.identity.userId !== void 0) prepared.userId = this.identity.userId;
640
1316
  if (this.identity.groupId !== void 0) prepared.groupId = this.identity.groupId;
@@ -658,7 +1334,17 @@ var DriveMetaDataSDK = class {
658
1334
  if (!validation.ok) {
659
1335
  this.lastError = `DMD SDK schema warning: ${validation.errors.join(", ")}`;
660
1336
  }
661
- this.sendEvent(this.toCollectorPayload(prepared));
1337
+ const collectorPayload = this.toCollectorPayload(prepared);
1338
+ const backendValidation = validateBackendCollectorPayload(collectorPayload);
1339
+ if (!backendValidation.ok && this.config.schemaValidation === "strict") {
1340
+ this.lastError = `DMD SDK backend schema warning: ${backendValidation.errors.join(", ")}`;
1341
+ this.recordDrop(type, "invalid_payload", event);
1342
+ return;
1343
+ }
1344
+ if (!backendValidation.ok) {
1345
+ this.lastError = `DMD SDK backend schema warning: ${backendValidation.errors.join(", ")}`;
1346
+ }
1347
+ this.sendEvent(collectorPayload);
662
1348
  }
663
1349
  toCollectorPayload(prepared) {
664
1350
  const browserWindow = getBrowserWindow();
@@ -674,14 +1360,14 @@ var DriveMetaDataSDK = class {
674
1360
  ua: browserWindow?.navigator?.userAgent,
675
1361
  appId: prepared.appId,
676
1362
  pageUrl: browserWindow?.location?.href,
677
- eventData: {
1363
+ eventData: mergeStoredAttribution({
678
1364
  ...prepared.properties,
679
1365
  anonymousId: prepared.anonymousId,
680
1366
  sessionId: prepared.sessionId,
681
1367
  timestamp: prepared.timestamp,
682
1368
  requestSentAt: prepared.timestamp,
683
1369
  requestReceivedAt: prepared.timestamp
684
- }
1370
+ }, this.persistence)
685
1371
  });
686
1372
  }
687
1373
  recordDrop(type, reason, event) {
@@ -757,7 +1443,7 @@ function initDmdSDK(config) {
757
1443
  return publicSingleton;
758
1444
  }
759
1445
  try {
760
- const instance = new DriveMetaDataSDK(config);
1446
+ const instance = new DriveMetaDataSDK(normalizeBrowserConfig(config));
761
1447
  return setSingleton(instance);
762
1448
  } catch (error) {
763
1449
  lastError = error instanceof Error ? error.message : String(error);
@@ -851,7 +1537,7 @@ function getDmdHealth() {
851
1537
  return health;
852
1538
  }
853
1539
  function resetDmdSDKForTests() {
854
- singleton?.reset();
1540
+ singleton?.disposeForTests();
855
1541
  singleton = void 0;
856
1542
  publicSingleton = void 0;
857
1543
  droppedEvents = 0;