@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.
@@ -13,12 +13,62 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
13
13
  // src/angular/dmd-analytics.service.ts
14
14
  import { Inject, Injectable, Optional } from "@angular/core";
15
15
 
16
+ // src/browser/core/browser-config.ts
17
+ function setIfDefined(target, key, value) {
18
+ if (value !== void 0) {
19
+ target[key] = value;
20
+ }
21
+ }
22
+ function normalizeBrowserConfig(input) {
23
+ const normalized = {
24
+ ...input,
25
+ clientId: input.clientId ?? "",
26
+ workspaceId: input.workspaceId ?? "",
27
+ appId: input.appId ?? ""
28
+ };
29
+ setIfDefined(normalized, "apiHost", input.apiHost);
30
+ setIfDefined(normalized, "capturePageview", input.capturePageview);
31
+ setIfDefined(normalized, "capturePageleave", input.capturePageleave);
32
+ setIfDefined(normalized, "captureDeadClicks", input.captureDeadClicks);
33
+ setIfDefined(normalized, "crossSubdomainCookie", input.crossSubdomainCookie);
34
+ setIfDefined(normalized, "sessionIdleTimeoutSeconds", input.sessionIdleTimeoutSeconds);
35
+ setIfDefined(normalized, "schemaValidation", input.schemaValidation);
36
+ setIfDefined(normalized, "beforeSend", input.beforeSend);
37
+ setIfDefined(normalized, "persistence", input.disablePersistence === true ? "none" : input.persistence);
38
+ return normalized;
39
+ }
40
+
41
+ // src/core/uuid.ts
42
+ function createUuid() {
43
+ const cryptoApi = globalThis.crypto;
44
+ if (typeof cryptoApi?.randomUUID === "function") {
45
+ return cryptoApi.randomUUID();
46
+ }
47
+ const bytes = new Uint8Array(16);
48
+ if (typeof cryptoApi?.getRandomValues === "function") {
49
+ cryptoApi.getRandomValues(bytes);
50
+ } else {
51
+ for (let index = 0; index < bytes.length; index += 1) {
52
+ bytes[index] = Math.floor(Math.random() * 256);
53
+ }
54
+ }
55
+ bytes[6] = (bytes[6] ?? 0) & 15 | 64;
56
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
57
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
58
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
59
+ }
60
+ function isUuid(value) {
61
+ 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);
62
+ }
63
+ function ensureUuid(value) {
64
+ return isUuid(value) ? value : createUuid();
65
+ }
66
+
16
67
  // src/core/backend-payload.ts
17
68
  function formatUtcTimestamp(value) {
18
- if (typeof value !== "string") return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
19
- const date = new Date(value);
20
- if (Number.isNaN(date.getTime())) return value;
21
- return date.toISOString().replace("T", " ").slice(0, 19);
69
+ const date = typeof value === "string" || typeof value === "number" || value instanceof Date ? new Date(value) : /* @__PURE__ */ new Date();
70
+ const safeDate = Number.isNaN(date.getTime()) ? /* @__PURE__ */ new Date() : date;
71
+ return safeDate.toISOString().replace("T", " ").slice(0, 19);
22
72
  }
23
73
  function cleanObject(value) {
24
74
  if (Array.isArray(value)) {
@@ -49,15 +99,19 @@ function createBackendCollectorPayload(input) {
49
99
  };
50
100
  const metaData = {
51
101
  ...normalizedEventData,
52
- requestId: input.requestId,
102
+ requestId: ensureUuid(typeof input.requestId === "string" ? input.requestId : void 0),
53
103
  timestamp,
54
104
  eventType: input.eventType,
55
105
  requestFrom: input.requestFrom ?? "3",
56
106
  clientId: input.clientId,
57
107
  workspaceId: input.workspaceId,
58
108
  token: input.token,
59
- anonymousId: eventData.anonymousId ?? input.anonymousId,
60
- sessionId: eventData.sessionId ?? input.sessionId,
109
+ anonymousId: ensureUuid(
110
+ typeof eventData.anonymousId === "string" ? eventData.anonymousId : typeof input.anonymousId === "string" ? input.anonymousId : void 0
111
+ ),
112
+ sessionId: ensureUuid(
113
+ typeof eventData.sessionId === "string" ? eventData.sessionId : typeof input.sessionId === "string" ? input.sessionId : void 0
114
+ ),
61
115
  ua: input.ua,
62
116
  appDetails: { app_id: input.appId },
63
117
  page: { ...page2, url: page2.url ?? input.pageUrl },
@@ -68,12 +122,45 @@ function createBackendCollectorPayload(input) {
68
122
  return cleanObject(payload);
69
123
  }
70
124
 
71
- // src/core/environment.ts
72
- function getBrowserWindow() {
73
- return typeof window === "undefined" ? void 0 : window;
125
+ // src/core/backend-schema.ts
126
+ var requiredMetaDataFields = [
127
+ "requestId",
128
+ "timestamp",
129
+ "eventType",
130
+ "requestFrom",
131
+ "clientId",
132
+ "workspaceId",
133
+ "anonymousId",
134
+ "sessionId"
135
+ ];
136
+ function isPresent(value) {
137
+ return value !== null && value !== void 0 && value !== "";
138
+ }
139
+ function validateBackendCollectorPayload(payload) {
140
+ const errors = [];
141
+ const metaData = payload.metaData;
142
+ if (!metaData || typeof metaData !== "object" || Array.isArray(metaData)) {
143
+ return {
144
+ ok: false,
145
+ errors: ["metaData is required"]
146
+ };
147
+ }
148
+ const metadataRecord = metaData;
149
+ for (const field of requiredMetaDataFields) {
150
+ if (!isPresent(metadataRecord[field])) {
151
+ errors.push(`metaData.${field} is required`);
152
+ }
153
+ }
154
+ if (isPresent(metadataRecord.eventType) && typeof metadataRecord.eventType !== "string") {
155
+ errors.push("metaData.eventType must be a string");
156
+ }
157
+ return {
158
+ ok: errors.length === 0,
159
+ errors
160
+ };
74
161
  }
75
162
 
76
- // src/browser/core/consent.ts
163
+ // src/core/consent.ts
77
164
  var purposes = [
78
165
  "analytics",
79
166
  "advertising",
@@ -113,6 +200,335 @@ function canCollectPurpose(consent, purpose) {
113
200
  return consent[purpose] === "granted";
114
201
  }
115
202
 
203
+ // src/core/event-schema.ts
204
+ var reservedKeys = /* @__PURE__ */ new Set(["messageId", "timestamp", "type", "event", "anonymousId", "userId", "context"]);
205
+ function validateEventEnvelope(envelope, options = {}) {
206
+ if (options.mode === "off") return { ok: true, errors: [] };
207
+ const errors = [];
208
+ if (typeof envelope.type !== "string") errors.push("type must be a string");
209
+ if (envelope.type === "track" && typeof envelope.event !== "string") {
210
+ errors.push("track event must include event name");
211
+ }
212
+ if (typeof envelope.messageId !== "string") errors.push("messageId must be a string");
213
+ if (typeof envelope.timestamp !== "string") errors.push("timestamp must be an ISO string");
214
+ for (const key of Object.keys(envelope.properties ?? {})) {
215
+ if (reservedKeys.has(key)) errors.push(`properties.${key} is reserved`);
216
+ }
217
+ return { ok: errors.length === 0, errors };
218
+ }
219
+
220
+ // src/core/environment.ts
221
+ function getBrowserWindow() {
222
+ return typeof window === "undefined" ? void 0 : window;
223
+ }
224
+
225
+ // src/core/privacy.ts
226
+ var sensitiveKeys = /* @__PURE__ */ new Set([
227
+ "email",
228
+ "phone",
229
+ "mobile",
230
+ "address",
231
+ "address1",
232
+ "address2",
233
+ "first_name",
234
+ "last_name",
235
+ "name",
236
+ "token",
237
+ "secret",
238
+ "password",
239
+ "session",
240
+ "cookie"
241
+ ]);
242
+ function sanitizeValue(value, allow, path = []) {
243
+ if (Array.isArray(value)) {
244
+ return value.map((item) => sanitizeValue(item, allow, path));
245
+ }
246
+ if (value && typeof value === "object") {
247
+ return Object.fromEntries(
248
+ Object.entries(value).filter(([key]) => {
249
+ const lowerKey = key.toLowerCase();
250
+ const lowerPath = path.map((item) => item.toLowerCase());
251
+ const isEcommerceItemName = lowerKey === "name" && lowerPath.includes("ecommerce") && lowerPath.includes("items");
252
+ return isEcommerceItemName || !sensitiveKeys.has(lowerKey) || allow.has(lowerKey);
253
+ }).map(([key, nestedValue]) => [key, sanitizeValue(nestedValue, allow, [...path, key])])
254
+ );
255
+ }
256
+ return value;
257
+ }
258
+ function sanitizeProperties(input, allowRawKeys = []) {
259
+ const allow = new Set(allowRawKeys.map((key) => key.toLowerCase()));
260
+ return sanitizeValue(input, allow);
261
+ }
262
+
263
+ // src/core/attribution.ts
264
+ var dmdUtmKeys = [
265
+ "utm_source",
266
+ "utm_medium",
267
+ "utm_campaign",
268
+ "utm_term",
269
+ "utm_content",
270
+ "utm_id"
271
+ ];
272
+ var dmdCampaignKeys = ["campaign_id", "ad_id"];
273
+ var dmdClickIdSources = [
274
+ ["gclid", 2],
275
+ ["fbclid", 3],
276
+ ["ScCid", 1],
277
+ ["li_fat_id", 4]
278
+ ];
279
+ function cleanAttributionRecord(record) {
280
+ const cleaned = Object.fromEntries(
281
+ Object.entries(record).filter(([, value]) => value !== null && value !== void 0 && value !== "")
282
+ );
283
+ return Object.keys(cleaned).length > 0 ? cleaned : void 0;
284
+ }
285
+ function objectValue(value) {
286
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
287
+ }
288
+ function mergeAttributionRecords(explicitProperties, stored) {
289
+ const attributionData = objectValue(explicitProperties.attributionData);
290
+ const utmParameter = objectValue(explicitProperties.utmParameter);
291
+ return {
292
+ ...explicitProperties,
293
+ ...stored.attributionData || attributionData ? { attributionData: { ...stored.attributionData ?? {}, ...attributionData ?? {} } } : {},
294
+ ...stored.utmParameter || utmParameter ? { utmParameter: { ...stored.utmParameter ?? {}, ...utmParameter ?? {} } } : {}
295
+ };
296
+ }
297
+
298
+ // src/browser/core/attribution.ts
299
+ function getCurrentUrl() {
300
+ return getBrowserWindow()?.location?.href;
301
+ }
302
+ function getCookie(name) {
303
+ const cookie = getBrowserWindow()?.document?.cookie;
304
+ if (!cookie) return void 0;
305
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
306
+ const match = new RegExp(`(?:^|; )${escapedName}=([^;]*)`).exec(cookie);
307
+ return match ? decodeURIComponent(match[1] ?? "") : void 0;
308
+ }
309
+ function getParams(url) {
310
+ return new URL(url, "https://placeholder.local").searchParams;
311
+ }
312
+ function setIfPresent(persistence, key, value) {
313
+ if (value !== null && value !== "") {
314
+ persistence.setItem(key, value);
315
+ }
316
+ }
317
+ function getStoredString(persistence, key) {
318
+ const value = persistence.getItem(key);
319
+ return value === null || value === "" ? void 0 : value;
320
+ }
321
+ function getStoredNumberOrString(persistence, key) {
322
+ const value = getStoredString(persistence, key);
323
+ if (value === void 0) return void 0;
324
+ const parsed = Number.parseInt(value, 10);
325
+ return Number.isNaN(parsed) ? value : parsed;
326
+ }
327
+ function captureAttributionFromUrl(persistence, url = getCurrentUrl()) {
328
+ if (!url) return;
329
+ const params = getParams(url);
330
+ for (const key of dmdUtmKeys) {
331
+ setIfPresent(persistence, key, params.get(key));
332
+ }
333
+ for (const key of dmdCampaignKeys) {
334
+ setIfPresent(persistence, key, params.get(key));
335
+ }
336
+ for (const [param, sdkPubId] of dmdClickIdSources) {
337
+ const value = params.get(param);
338
+ if (value) {
339
+ persistence.setItem("unique_id", value);
340
+ persistence.setItem("sdk_pub_id", String(sdkPubId));
341
+ }
342
+ }
343
+ }
344
+ function getStoredAttributionData(persistence) {
345
+ return cleanAttributionRecord({
346
+ unique_id: getStoredString(persistence, "unique_id"),
347
+ sdk_pub_id: getStoredNumberOrString(persistence, "sdk_pub_id"),
348
+ fbc: getStoredString(persistence, "fbc") ?? getCookie("_fbc"),
349
+ fbp: getStoredString(persistence, "fbp") ?? getCookie("_fbp"),
350
+ campaign_id: getStoredString(persistence, "campaign_id"),
351
+ ad_id: getStoredString(persistence, "ad_id")
352
+ });
353
+ }
354
+ function getStoredUtmParameter(persistence) {
355
+ return cleanAttributionRecord(Object.fromEntries(dmdUtmKeys.map((key) => [key, getStoredString(persistence, key)])));
356
+ }
357
+ function mergeStoredAttribution(properties, persistence) {
358
+ const stored = {};
359
+ const attributionData = getStoredAttributionData(persistence);
360
+ const utmParameter = getStoredUtmParameter(persistence);
361
+ if (attributionData !== void 0) stored.attributionData = attributionData;
362
+ if (utmParameter !== void 0) stored.utmParameter = utmParameter;
363
+ return mergeAttributionRecords(properties, stored);
364
+ }
365
+
366
+ // src/browser/core/autocapture.ts
367
+ function installAutocapture(config) {
368
+ const trackedPageUrls = /* @__PURE__ */ new Set();
369
+ const timers = /* @__PURE__ */ new Set();
370
+ const pageviewDelayMs = config.pageviewDelayMs ?? 500;
371
+ const history = config.browserWindow.history;
372
+ const originalPushState = history?.pushState;
373
+ const originalReplaceState = history?.replaceState;
374
+ function clearTimer(timer) {
375
+ timers.delete(timer);
376
+ clearTimeout(timer);
377
+ }
378
+ function canonicalUrl() {
379
+ return config.browserWindow.location.href;
380
+ }
381
+ function trackCurrentPageview() {
382
+ if (!config.capturePageview) return;
383
+ const url = canonicalUrl();
384
+ if (trackedPageUrls.has(url)) return;
385
+ trackedPageUrls.add(url);
386
+ config.onPageView();
387
+ }
388
+ function schedulePageview(delayMs) {
389
+ if (!config.capturePageview) return;
390
+ const timer = setTimeout(() => {
391
+ timers.delete(timer);
392
+ trackCurrentPageview();
393
+ }, delayMs);
394
+ timers.add(timer);
395
+ }
396
+ function handleRouteChange() {
397
+ config.onRouteChange?.();
398
+ trackCurrentPageview();
399
+ }
400
+ function wrapHistoryMethod(method) {
401
+ if (!history) return;
402
+ const original = history[method];
403
+ if (typeof original !== "function") return;
404
+ history[method] = function wrappedHistoryMethod(...args) {
405
+ const result = original.apply(this, args);
406
+ const timer = setTimeout(() => {
407
+ timers.delete(timer);
408
+ handleRouteChange();
409
+ }, 0);
410
+ timers.add(timer);
411
+ return result;
412
+ };
413
+ }
414
+ function handlePopOrHashChange() {
415
+ handleRouteChange();
416
+ }
417
+ function handlePageLeave() {
418
+ if (config.capturePageleave) {
419
+ config.onPageLeave();
420
+ }
421
+ }
422
+ schedulePageview(pageviewDelayMs);
423
+ wrapHistoryMethod("pushState");
424
+ wrapHistoryMethod("replaceState");
425
+ const canListen = typeof config.browserWindow.addEventListener === "function" && typeof config.browserWindow.removeEventListener === "function";
426
+ if (canListen) {
427
+ config.browserWindow.addEventListener("popstate", handlePopOrHashChange);
428
+ config.browserWindow.addEventListener("hashchange", handlePopOrHashChange);
429
+ config.browserWindow.addEventListener("beforeunload", handlePageLeave);
430
+ }
431
+ return {
432
+ cleanup() {
433
+ for (const timer of Array.from(timers)) {
434
+ clearTimer(timer);
435
+ }
436
+ if (history && originalPushState) history.pushState = originalPushState;
437
+ if (history && originalReplaceState) history.replaceState = originalReplaceState;
438
+ if (canListen) {
439
+ config.browserWindow.removeEventListener("popstate", handlePopOrHashChange);
440
+ config.browserWindow.removeEventListener("hashchange", handlePopOrHashChange);
441
+ config.browserWindow.removeEventListener("beforeunload", handlePageLeave);
442
+ }
443
+ }
444
+ };
445
+ }
446
+
447
+ // src/core/payload-size.ts
448
+ var preserveStringKeys = /* @__PURE__ */ new Set([
449
+ "requestId",
450
+ "eventType",
451
+ "requestFrom",
452
+ "clientId",
453
+ "workspaceId",
454
+ "anonymousId",
455
+ "sessionId",
456
+ "token"
457
+ ]);
458
+ function payloadByteLength(payload) {
459
+ const serialized = JSON.stringify(payload);
460
+ if (typeof TextEncoder !== "undefined") {
461
+ return new TextEncoder().encode(serialized).length;
462
+ }
463
+ return serialized.length;
464
+ }
465
+ function clonePayload(payload) {
466
+ try {
467
+ return JSON.parse(JSON.stringify(payload));
468
+ } catch {
469
+ return void 0;
470
+ }
471
+ }
472
+ function truncateValue(value, truncateStringLength, key) {
473
+ if (typeof value === "string") {
474
+ if (key && preserveStringKeys.has(key)) return value;
475
+ if (value.length <= truncateStringLength) return value;
476
+ return `${value.slice(0, truncateStringLength)}...[TRUNCATED]`;
477
+ }
478
+ if (Array.isArray(value)) {
479
+ return value.map((item) => truncateValue(item, truncateStringLength));
480
+ }
481
+ if (value && typeof value === "object") {
482
+ return Object.fromEntries(
483
+ Object.entries(value).map(([childKey, childValue]) => [
484
+ childKey,
485
+ truncateValue(childValue, truncateStringLength, childKey)
486
+ ])
487
+ );
488
+ }
489
+ return value;
490
+ }
491
+ function markPayloadTruncated(payload) {
492
+ if (payload.metaData && typeof payload.metaData === "object" && !Array.isArray(payload.metaData)) {
493
+ return {
494
+ ...payload,
495
+ metaData: {
496
+ ...payload.metaData,
497
+ payloadTruncated: true
498
+ }
499
+ };
500
+ }
501
+ return {
502
+ ...payload,
503
+ payloadTruncated: true
504
+ };
505
+ }
506
+ function truncatePayload(payload, truncateStringLength) {
507
+ const cloned = clonePayload(payload);
508
+ if (!cloned) return void 0;
509
+ return markPayloadTruncated(truncateValue(cloned, truncateStringLength));
510
+ }
511
+ function getPayloadMessageId(payload) {
512
+ const value = payload.metaData?.requestId ?? payload.messageId;
513
+ return value === void 0 ? void 0 : String(value);
514
+ }
515
+ function preparePayloadForSizePolicy(payload, options = {}) {
516
+ const maxPayloadBytes = options.maxPayloadBytes ?? 64e3;
517
+ const payloadSizePolicy = options.payloadSizePolicy ?? "drop";
518
+ const payloadTruncateStringLength = options.payloadTruncateStringLength ?? 1024;
519
+ if (payloadByteLength(payload) <= maxPayloadBytes) {
520
+ return { ok: true, payload, truncated: false };
521
+ }
522
+ if (payloadSizePolicy === "truncate") {
523
+ const truncatedPayload = truncatePayload(payload, payloadTruncateStringLength);
524
+ if (truncatedPayload && payloadByteLength(truncatedPayload) <= maxPayloadBytes) {
525
+ return { ok: true, payload: truncatedPayload, truncated: true };
526
+ }
527
+ }
528
+ const messageId = getPayloadMessageId(payload);
529
+ return messageId === void 0 ? { ok: false, reason: "payload_too_large" } : { ok: false, reason: "payload_too_large", messageId };
530
+ }
531
+
116
532
  // src/browser/core/delivery.ts
117
533
  function createId(prefix) {
118
534
  return `${prefix}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
@@ -144,7 +560,6 @@ function createDeliveryManager(config) {
144
560
  const lockTtlMs = config.lockTtlMs ?? 5e3;
145
561
  const tabId = config.tabId ?? createId("tab");
146
562
  const batchSize = config.batchSize ?? 25;
147
- const maxPayloadBytes = config.maxPayloadBytes ?? 64e3;
148
563
  function recordDrop(event) {
149
564
  diagnostics.dropped.push(event);
150
565
  config.onDrop?.(event);
@@ -153,12 +568,22 @@ function createDeliveryManager(config) {
153
568
  diagnostics.lastError = error.message;
154
569
  config.onError?.(error);
155
570
  }
156
- function payloadByteLength(payload) {
157
- const serialized = JSON.stringify(payload);
158
- if (typeof TextEncoder !== "undefined") {
159
- return new TextEncoder().encode(serialized).length;
571
+ function preparePayloadForSend(payload) {
572
+ const sizePolicyOptions = {};
573
+ if (config.maxPayloadBytes !== void 0) sizePolicyOptions.maxPayloadBytes = config.maxPayloadBytes;
574
+ if (config.payloadSizePolicy !== void 0) sizePolicyOptions.payloadSizePolicy = config.payloadSizePolicy;
575
+ if (config.payloadTruncateStringLength !== void 0) {
576
+ sizePolicyOptions.payloadTruncateStringLength = config.payloadTruncateStringLength;
160
577
  }
161
- return serialized.length;
578
+ const prepared = preparePayloadForSizePolicy(payload, sizePolicyOptions);
579
+ if (prepared.ok) return prepared.payload;
580
+ const diagnostic = {
581
+ reason: prepared.reason,
582
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
583
+ };
584
+ if (prepared.messageId !== void 0) diagnostic.messageId = prepared.messageId;
585
+ recordDrop(diagnostic);
586
+ return void 0;
162
587
  }
163
588
  function recordStorageUnavailable() {
164
589
  recordDrop({
@@ -248,7 +673,19 @@ function createDeliveryManager(config) {
248
673
  idempotencyKey: String(payload.idempotencyKey ?? createIdempotencyKey(payload, messageId))
249
674
  };
250
675
  }
676
+ function tryBeacon(body) {
677
+ if (!config.useBeacon) return false;
678
+ const sendBeacon = globalThis.navigator?.sendBeacon;
679
+ if (typeof sendBeacon !== "function") return false;
680
+ try {
681
+ const blob = new Blob([JSON.stringify(body)], { type: "application/json" });
682
+ return sendBeacon.call(globalThis.navigator, config.endpoint, blob);
683
+ } catch {
684
+ return false;
685
+ }
686
+ }
251
687
  async function deliver(body) {
688
+ if (tryBeacon(body)) return;
252
689
  const fetchImpl = config.fetch ?? globalThis.fetch;
253
690
  if (typeof fetchImpl !== "function") {
254
691
  throw new Error("fetch_unavailable");
@@ -265,26 +702,22 @@ function createDeliveryManager(config) {
265
702
  return {
266
703
  async send(payload) {
267
704
  const body = withEnvelope(payload);
268
- if (payloadByteLength(body) > maxPayloadBytes) {
269
- recordDrop({
270
- messageId: String(body.metaData?.requestId ?? body.messageId),
271
- reason: "payload_too_large",
272
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
273
- });
705
+ const preparedBody = preparePayloadForSend(body);
706
+ if (!preparedBody) {
274
707
  return;
275
708
  }
276
709
  diagnostics.inFlight += 1;
277
710
  try {
278
- await deliver(body);
711
+ await deliver(preparedBody);
279
712
  } catch (error) {
280
713
  const deliveryError = error instanceof Error ? error : new Error(String(error));
281
714
  recordError(deliveryError);
282
715
  enqueue({
283
- messageId: String(body.metaData?.requestId ?? body.messageId),
716
+ messageId: String(getPayloadMessageId(body)),
284
717
  savedAt: Date.now(),
285
718
  attempts: 1,
286
719
  lastError: deliveryError.message,
287
- payload: body
720
+ payload: preparedBody
288
721
  });
289
722
  } finally {
290
723
  diagnostics.inFlight -= 1;
@@ -397,60 +830,268 @@ function createDeliveryManager(config) {
397
830
  };
398
831
  }
399
832
 
400
- // src/browser/core/privacy.ts
401
- var sensitiveKeys = /* @__PURE__ */ new Set([
402
- "email",
403
- "phone",
404
- "mobile",
405
- "address",
406
- "address1",
407
- "address2",
408
- "first_name",
409
- "last_name",
410
- "name",
411
- "token",
412
- "secret",
413
- "password",
414
- "session",
415
- "cookie"
416
- ]);
417
- function sanitizeValue(value, allow) {
418
- if (Array.isArray(value)) {
419
- return value.map((item) => sanitizeValue(item, allow));
833
+ // src/browser/core/identity.ts
834
+ var ANONYMOUS_ID_STORAGE_KEY = "dmd_anonymous_id";
835
+ var LEGACY_ANONYMOUS_ID_STORAGE_KEY = "anonymousId";
836
+ function getUrlParam(name) {
837
+ const href = getBrowserWindow()?.location?.href;
838
+ if (!href) return null;
839
+ try {
840
+ return new URL(href).searchParams.get(name);
841
+ } catch {
842
+ return null;
420
843
  }
421
- if (value && typeof value === "object") {
422
- return Object.fromEntries(
423
- Object.entries(value).filter(([key]) => !sensitiveKeys.has(key.toLowerCase()) || allow.has(key.toLowerCase())).map(([key, nestedValue]) => [key, sanitizeValue(nestedValue, allow)])
424
- );
844
+ }
845
+ function resolveAnonymousId(persistence) {
846
+ const urlAnonymousId = getUrlParam("aid");
847
+ if (isUuid(urlAnonymousId)) {
848
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, urlAnonymousId);
849
+ return urlAnonymousId;
850
+ }
851
+ const storedAnonymousId = persistence.getItem(ANONYMOUS_ID_STORAGE_KEY);
852
+ if (isUuid(storedAnonymousId)) {
853
+ return storedAnonymousId;
854
+ }
855
+ const legacyAnonymousId = persistence.getItem(LEGACY_ANONYMOUS_ID_STORAGE_KEY);
856
+ if (isUuid(legacyAnonymousId)) {
857
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, legacyAnonymousId);
858
+ return legacyAnonymousId;
859
+ }
860
+ const anonymousId = createUuid();
861
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, anonymousId);
862
+ return anonymousId;
863
+ }
864
+ function resetAnonymousId(persistence) {
865
+ const anonymousId = createUuid();
866
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, anonymousId);
867
+ return anonymousId;
868
+ }
869
+
870
+ // src/browser/core/persistence.ts
871
+ function createPersistenceHealth(requested) {
872
+ return {
873
+ requested,
874
+ cookieFallbackUsed: false,
875
+ memoryFallbackUsed: requested === "memory",
876
+ failures: []
877
+ };
878
+ }
879
+ function recordFailure(health, backend, operation, error) {
880
+ health.failures.push({
881
+ backend,
882
+ operation,
883
+ message: error instanceof Error ? error.message : String(error)
884
+ });
885
+ if (health.failures.length > 25) {
886
+ health.failures.shift();
425
887
  }
426
- return value;
427
888
  }
428
- function sanitizeProperties(input, allowRawKeys = []) {
429
- const allow = new Set(allowRawKeys.map((key) => key.toLowerCase()));
430
- return sanitizeValue(input, allow);
889
+ function createMemoryPersistence(health = createPersistenceHealth("memory")) {
890
+ const memory = {};
891
+ return {
892
+ getItem(key) {
893
+ return Object.prototype.hasOwnProperty.call(memory, key) ? memory[key] ?? null : null;
894
+ },
895
+ setItem(key, value) {
896
+ memory[key] = value;
897
+ return true;
898
+ },
899
+ removeItem(key) {
900
+ delete memory[key];
901
+ },
902
+ getHealth() {
903
+ return {
904
+ ...health,
905
+ failures: health.failures.map((failure) => ({ ...failure }))
906
+ };
907
+ }
908
+ };
909
+ }
910
+ function getCookie2(name) {
911
+ const browserWindow = getBrowserWindow();
912
+ const cookie = browserWindow?.document?.cookie;
913
+ if (!cookie) return null;
914
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
915
+ const match = new RegExp(`(?:^|; )${escapedName}=([^;]*)`).exec(cookie);
916
+ return match ? decodeURIComponent(match[1] ?? "") : null;
917
+ }
918
+ function setCookie(name, value) {
919
+ const browserWindow = getBrowserWindow();
920
+ if (!browserWindow?.document) return false;
921
+ try {
922
+ const expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3).toUTCString();
923
+ const secure = browserWindow.location?.protocol === "https:" ? "; Secure" : "";
924
+ browserWindow.document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/${secure}; SameSite=Lax`;
925
+ return true;
926
+ } catch {
927
+ return false;
928
+ }
929
+ }
930
+ function removeCookie(name) {
931
+ const browserWindow = getBrowserWindow();
932
+ if (!browserWindow?.document) return;
933
+ browserWindow.document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
934
+ }
935
+ function getLocalStorage() {
936
+ const browserWindow = getBrowserWindow();
937
+ try {
938
+ return browserWindow?.localStorage;
939
+ } catch {
940
+ return void 0;
941
+ }
942
+ }
943
+ function getSessionStorage() {
944
+ const browserWindow = getBrowserWindow();
945
+ try {
946
+ return browserWindow?.sessionStorage;
947
+ } catch {
948
+ return void 0;
949
+ }
950
+ }
951
+ function createBrowserPersistence(mode = "localStorage+cookie") {
952
+ const health = createPersistenceHealth(mode);
953
+ const memory = createMemoryPersistence(health);
954
+ if (mode === "none") {
955
+ return {
956
+ getItem() {
957
+ return null;
958
+ },
959
+ setItem() {
960
+ return false;
961
+ },
962
+ removeItem() {
963
+ },
964
+ getHealth() {
965
+ return {
966
+ ...health,
967
+ failures: health.failures.map((failure) => ({ ...failure }))
968
+ };
969
+ }
970
+ };
971
+ }
972
+ if (mode === "memory") return memory;
973
+ const primary = mode === "sessionStorage" ? getSessionStorage() : getLocalStorage();
974
+ const useCookie = mode === "cookie" || mode === "localStorage+cookie" || !primary;
975
+ return {
976
+ getItem(key) {
977
+ try {
978
+ const stored = primary?.getItem(key);
979
+ if (stored) return stored;
980
+ } catch (error) {
981
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "get", error);
982
+ }
983
+ if (useCookie) {
984
+ const cookie = getCookie2(key);
985
+ if (cookie) {
986
+ health.cookieFallbackUsed = true;
987
+ return cookie;
988
+ }
989
+ }
990
+ const value = memory.getItem(key);
991
+ if (value !== null) health.memoryFallbackUsed = true;
992
+ return value;
993
+ },
994
+ setItem(key, value) {
995
+ let wrote = false;
996
+ try {
997
+ primary?.setItem(key, value);
998
+ wrote = primary !== void 0;
999
+ } catch (error) {
1000
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "set", error);
1001
+ wrote = false;
1002
+ }
1003
+ if (useCookie) {
1004
+ const cookieWrote = setCookie(key, value);
1005
+ if (cookieWrote) health.cookieFallbackUsed = true;
1006
+ wrote = cookieWrote || wrote;
1007
+ }
1008
+ if (!wrote) {
1009
+ health.memoryFallbackUsed = true;
1010
+ return memory.setItem(key, value);
1011
+ }
1012
+ if (mode === "localStorage+cookie") {
1013
+ memory.setItem(key, value);
1014
+ }
1015
+ return true;
1016
+ },
1017
+ removeItem(key) {
1018
+ try {
1019
+ primary?.removeItem(key);
1020
+ } catch (error) {
1021
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "remove", error);
1022
+ }
1023
+ if (useCookie) removeCookie(key);
1024
+ memory.removeItem(key);
1025
+ },
1026
+ getHealth() {
1027
+ return {
1028
+ ...health,
1029
+ failures: health.failures.map((failure) => ({ ...failure }))
1030
+ };
1031
+ }
1032
+ };
431
1033
  }
432
1034
 
433
- // src/browser/core/schema.ts
434
- var reservedKeys = /* @__PURE__ */ new Set(["messageId", "timestamp", "type", "event", "anonymousId", "userId", "context"]);
435
- function validateEventEnvelope(envelope, options = {}) {
436
- if (options.mode === "off") return { ok: true, errors: [] };
437
- const errors = [];
438
- if (typeof envelope.type !== "string") errors.push("type must be a string");
439
- if (envelope.type === "track" && typeof envelope.event !== "string") {
440
- errors.push("track event must include event name");
1035
+ // src/browser/core/session.ts
1036
+ var SESSION_STORAGE_KEY = "sessionData";
1037
+ function getUrlParam2(name) {
1038
+ const href = getBrowserWindow()?.location?.href;
1039
+ if (!href) return null;
1040
+ try {
1041
+ return new URL(href).searchParams.get(name);
1042
+ } catch {
1043
+ return null;
441
1044
  }
442
- if (typeof envelope.messageId !== "string") errors.push("messageId must be a string");
443
- if (typeof envelope.timestamp !== "string") errors.push("timestamp must be an ISO string");
444
- for (const key of Object.keys(envelope.properties ?? {})) {
445
- if (reservedKeys.has(key)) errors.push(`properties.${key} is reserved`);
1045
+ }
1046
+ function readStoredSession(persistence) {
1047
+ const rawSession = persistence.getItem(SESSION_STORAGE_KEY);
1048
+ if (!rawSession) return void 0;
1049
+ try {
1050
+ const parsed = JSON.parse(rawSession);
1051
+ if (isUuid(parsed.sessionId) && typeof parsed.timestamp === "number") {
1052
+ return {
1053
+ sessionId: parsed.sessionId,
1054
+ timestamp: parsed.timestamp
1055
+ };
1056
+ }
1057
+ } catch {
1058
+ persistence.removeItem(SESSION_STORAGE_KEY);
446
1059
  }
447
- return { ok: errors.length === 0, errors };
1060
+ return void 0;
1061
+ }
1062
+ function writeStoredSession(persistence, sessionId, timestamp) {
1063
+ persistence.setItem(SESSION_STORAGE_KEY, JSON.stringify({ sessionId, timestamp }));
1064
+ }
1065
+ function createSessionManager(persistence, idleTimeoutSeconds = 30 * 60) {
1066
+ function resolveSessionId() {
1067
+ const now = Date.now();
1068
+ const urlSessionId = getUrlParam2("sid") ?? getUrlParam2("session_id");
1069
+ if (isUuid(urlSessionId)) {
1070
+ writeStoredSession(persistence, urlSessionId, now);
1071
+ return urlSessionId;
1072
+ }
1073
+ const storedSession = readStoredSession(persistence);
1074
+ if (storedSession && now - storedSession.timestamp <= idleTimeoutSeconds * 1e3) {
1075
+ writeStoredSession(persistence, storedSession.sessionId, now);
1076
+ return storedSession.sessionId;
1077
+ }
1078
+ const sessionId = createUuid();
1079
+ writeStoredSession(persistence, sessionId, now);
1080
+ return sessionId;
1081
+ }
1082
+ return {
1083
+ getSessionId() {
1084
+ return resolveSessionId();
1085
+ },
1086
+ reset() {
1087
+ const sessionId = createUuid();
1088
+ writeStoredSession(persistence, sessionId, Date.now());
1089
+ return sessionId;
1090
+ }
1091
+ };
448
1092
  }
449
1093
 
450
1094
  // src/browser/core/DriveMetaDataSDK.ts
451
- function createId2(prefix) {
452
- return `${prefix}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
453
- }
454
1095
  function endpointFromConfig(config) {
455
1096
  const host = config.apiHost ?? "https://sdk.drivemetadata.com/v2";
456
1097
  return `${host.replace(/\/$/, "")}/data-collector`;
@@ -461,14 +1102,6 @@ function requireConfigString(value, field) {
461
1102
  }
462
1103
  return value;
463
1104
  }
464
- function getBrowserStorage() {
465
- const browserWindow = getBrowserWindow();
466
- try {
467
- return browserWindow?.localStorage;
468
- } catch {
469
- return void 0;
470
- }
471
- }
472
1105
  var DriveMetaDataSDK = class {
473
1106
  constructor(config) {
474
1107
  this.initialized = true;
@@ -481,30 +1114,35 @@ var DriveMetaDataSDK = class {
481
1114
  requireConfigString(config.writeKey || config.token, "writeKey or token");
482
1115
  this.config = config;
483
1116
  this.endpoint = endpointFromConfig(config);
484
- const storage = getBrowserStorage();
1117
+ this.persistence = createBrowserPersistence(config.persistence);
1118
+ this.session = createSessionManager(this.persistence, config.sessionIdleTimeoutSeconds);
485
1119
  const deliveryConfig = {
486
- endpoint: this.endpoint
1120
+ endpoint: this.endpoint,
1121
+ storage: this.persistence
487
1122
  };
488
1123
  if (config.delivery?.maxQueueSize !== void 0) deliveryConfig.maxQueueSize = config.delivery.maxQueueSize;
489
1124
  if (config.delivery?.queueTtlMs !== void 0) deliveryConfig.queueTtlMs = config.delivery.queueTtlMs;
490
1125
  if (config.delivery?.retryDelayMs !== void 0) deliveryConfig.retryDelayMs = config.delivery.retryDelayMs;
491
1126
  if (config.delivery?.maxRetryDelayMs !== void 0) deliveryConfig.maxRetryDelayMs = config.delivery.maxRetryDelayMs;
492
1127
  if (config.delivery?.maxPayloadBytes !== void 0) deliveryConfig.maxPayloadBytes = config.delivery.maxPayloadBytes;
1128
+ if (config.delivery?.payloadSizePolicy !== void 0) deliveryConfig.payloadSizePolicy = config.delivery.payloadSizePolicy;
1129
+ if (config.delivery?.payloadTruncateStringLength !== void 0) {
1130
+ deliveryConfig.payloadTruncateStringLength = config.delivery.payloadTruncateStringLength;
1131
+ }
1132
+ if (config.delivery?.useBeacon !== void 0) deliveryConfig.useBeacon = config.delivery.useBeacon;
493
1133
  if (config.delivery?.batchSize !== void 0) deliveryConfig.batchSize = config.delivery.batchSize;
494
1134
  if (config.onDrop !== void 0) deliveryConfig.onDrop = config.onDrop;
495
1135
  if (config.onError !== void 0) deliveryConfig.onError = config.onError;
496
- this.delivery = createDeliveryManager(storage ? {
497
- ...deliveryConfig,
498
- storage
499
- } : deliveryConfig);
1136
+ this.delivery = createDeliveryManager(deliveryConfig);
500
1137
  this.initialRetryDelayMs = config.delivery?.retryDelayMs ?? 1e3;
501
1138
  this.retryDelayMs = this.initialRetryDelayMs;
502
1139
  this.maxRetryDelayMs = config.delivery?.maxRetryDelayMs ?? 3e4;
503
1140
  this.writeKey = config.writeKey || config.token || "";
504
- this.identity = { anonymousId: createId2("anon") };
505
- this.sessionId = createId2("session");
1141
+ this.identity = { anonymousId: resolveAnonymousId(this.persistence) };
1142
+ captureAttributionFromUrl(this.persistence);
506
1143
  this.consentState = normalizeConsent(config.gdprConsent ?? config.consent);
507
1144
  this.gdprConsent = this.consentState.analytics;
1145
+ this.installAutocapture();
508
1146
  if (!config.delivery?.disableLifecycleFlush) {
509
1147
  this.installLifecycleFlush();
510
1148
  }
@@ -544,7 +1182,8 @@ var DriveMetaDataSDK = class {
544
1182
  }
545
1183
  }
546
1184
  reset() {
547
- this.identity = { anonymousId: createId2("anon") };
1185
+ this.identity = { anonymousId: resetAnonymousId(this.persistence) };
1186
+ this.session.reset();
548
1187
  this.queue = [];
549
1188
  this.offline = false;
550
1189
  if (this.retryTimer !== void 0) {
@@ -553,8 +1192,20 @@ var DriveMetaDataSDK = class {
553
1192
  }
554
1193
  this.lifecycleCleanup?.();
555
1194
  this.lifecycleCleanup = void 0;
1195
+ this.autocaptureCleanup?.();
1196
+ this.autocaptureCleanup = void 0;
556
1197
  this.delivery.clearQueue("manual_clear");
557
1198
  }
1199
+ disposeForTests() {
1200
+ if (this.retryTimer !== void 0) {
1201
+ clearTimeout(this.retryTimer);
1202
+ this.retryTimer = void 0;
1203
+ }
1204
+ this.lifecycleCleanup?.();
1205
+ this.lifecycleCleanup = void 0;
1206
+ this.autocaptureCleanup?.();
1207
+ this.autocaptureCleanup = void 0;
1208
+ }
558
1209
  setConsent(consent) {
559
1210
  this.consentState = typeof consent === "object" ? mergeConsent(this.consentState, consent) : normalizeConsent(consent);
560
1211
  this.gdprConsent = this.consentState.analytics;
@@ -569,6 +1220,7 @@ var DriveMetaDataSDK = class {
569
1220
  initialized: this.initialized,
570
1221
  consent: this.gdprConsent,
571
1222
  consentPurposes: this.consentState,
1223
+ persistence: this.persistence.getHealth(),
572
1224
  queueSize: deliveryDiagnostics.queued,
573
1225
  offline: this.offline || deliveryDiagnostics.queued > 0,
574
1226
  droppedEvents: this.droppedEvents + deliveryDiagnostics.dropped.length,
@@ -583,7 +1235,9 @@ var DriveMetaDataSDK = class {
583
1235
  void this.delivery.send(payload).then(() => {
584
1236
  const diagnostics = this.delivery.getDiagnostics();
585
1237
  this.offline = diagnostics.queued > 0;
586
- this.lastError = diagnostics.lastError;
1238
+ if (diagnostics.lastError !== void 0) {
1239
+ this.lastError = diagnostics.lastError;
1240
+ }
587
1241
  if (diagnostics.queued > 0) {
588
1242
  this.scheduleRetryFlush();
589
1243
  }
@@ -615,6 +1269,28 @@ var DriveMetaDataSDK = class {
615
1269
  }
616
1270
  };
617
1271
  }
1272
+ installAutocapture() {
1273
+ const browserWindow = getBrowserWindow();
1274
+ if (!browserWindow) return;
1275
+ if (this.config.autocapture === false) return;
1276
+ const capturePageview = this.config.capturePageview !== false;
1277
+ const capturePageleave = this.config.capturePageleave !== false;
1278
+ const controller = installAutocapture({
1279
+ browserWindow,
1280
+ capturePageview,
1281
+ capturePageleave,
1282
+ onPageView: () => {
1283
+ this.page();
1284
+ },
1285
+ onPageLeave: () => {
1286
+ this.trackEvent("page_leave", { url: browserWindow.location.href });
1287
+ },
1288
+ onRouteChange: () => {
1289
+ captureAttributionFromUrl(this.persistence);
1290
+ }
1291
+ });
1292
+ this.autocaptureCleanup = controller.cleanup;
1293
+ }
618
1294
  scheduleRetryFlush() {
619
1295
  if (this.retryTimer !== void 0) return;
620
1296
  const delay = Math.min(this.retryDelayMs, this.maxRetryDelayMs);
@@ -640,7 +1316,7 @@ var DriveMetaDataSDK = class {
640
1316
  type,
641
1317
  event,
642
1318
  properties: sanitizeProperties(properties),
643
- messageId: options.messageId ?? createId2("msg"),
1319
+ messageId: ensureUuid(options.messageId),
644
1320
  timestamp: options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
645
1321
  context: options.context ?? {},
646
1322
  anonymousId: this.identity.anonymousId,
@@ -649,7 +1325,7 @@ var DriveMetaDataSDK = class {
649
1325
  appId: this.config.appId,
650
1326
  writeKey: this.writeKey,
651
1327
  consent: this.consentState,
652
- sessionId: this.sessionId
1328
+ sessionId: this.session.getSessionId()
653
1329
  };
654
1330
  if (this.identity.userId !== void 0) prepared.userId = this.identity.userId;
655
1331
  if (this.identity.groupId !== void 0) prepared.groupId = this.identity.groupId;
@@ -673,7 +1349,17 @@ var DriveMetaDataSDK = class {
673
1349
  if (!validation.ok) {
674
1350
  this.lastError = `DMD SDK schema warning: ${validation.errors.join(", ")}`;
675
1351
  }
676
- this.sendEvent(this.toCollectorPayload(prepared));
1352
+ const collectorPayload = this.toCollectorPayload(prepared);
1353
+ const backendValidation = validateBackendCollectorPayload(collectorPayload);
1354
+ if (!backendValidation.ok && this.config.schemaValidation === "strict") {
1355
+ this.lastError = `DMD SDK backend schema warning: ${backendValidation.errors.join(", ")}`;
1356
+ this.recordDrop(type, "invalid_payload", event);
1357
+ return;
1358
+ }
1359
+ if (!backendValidation.ok) {
1360
+ this.lastError = `DMD SDK backend schema warning: ${backendValidation.errors.join(", ")}`;
1361
+ }
1362
+ this.sendEvent(collectorPayload);
677
1363
  }
678
1364
  toCollectorPayload(prepared) {
679
1365
  const browserWindow = getBrowserWindow();
@@ -689,14 +1375,14 @@ var DriveMetaDataSDK = class {
689
1375
  ua: browserWindow?.navigator?.userAgent,
690
1376
  appId: prepared.appId,
691
1377
  pageUrl: browserWindow?.location?.href,
692
- eventData: {
1378
+ eventData: mergeStoredAttribution({
693
1379
  ...prepared.properties,
694
1380
  anonymousId: prepared.anonymousId,
695
1381
  sessionId: prepared.sessionId,
696
1382
  timestamp: prepared.timestamp,
697
1383
  requestSentAt: prepared.timestamp,
698
1384
  requestReceivedAt: prepared.timestamp
699
- }
1385
+ }, this.persistence)
700
1386
  });
701
1387
  }
702
1388
  recordDrop(type, reason, event) {
@@ -772,7 +1458,7 @@ function initDmdSDK(config) {
772
1458
  return publicSingleton;
773
1459
  }
774
1460
  try {
775
- const instance = new DriveMetaDataSDK(config);
1461
+ const instance = new DriveMetaDataSDK(normalizeBrowserConfig(config));
776
1462
  return setSingleton(instance);
777
1463
  } catch (error) {
778
1464
  lastError = error instanceof Error ? error.message : String(error);