@chordcommerce/analytics 1.19.0-beta.1 → 1.21.0-beta.3

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.
@@ -394,3 +394,248 @@ export interface PaymentInfoEnteredInput<T extends ObjectTypes = ObjectTypes> {
394
394
  */
395
395
  value?: number;
396
396
  }
397
+ export interface OrderCompletedProductInput {
398
+ /**
399
+ * Product affiliation (for example, "Google Store")
400
+ */
401
+ affiliation?: string;
402
+ /**
403
+ * Brand associated with the product
404
+ */
405
+ brand?: string;
406
+ /**
407
+ * Product belongs to a bundle
408
+ */
409
+ bundle?: boolean;
410
+ /**
411
+ * Number of SKUs in the bundle
412
+ */
413
+ bundleSize?: number;
414
+ /**
415
+ * Product category
416
+ */
417
+ category?: string;
418
+ /**
419
+ * Coupon code associated with the product
420
+ */
421
+ coupon?: string;
422
+ /**
423
+ * Quantity sold via cross-selling
424
+ */
425
+ crossSoldQuantity?: number;
426
+ /**
427
+ * Short description of the product
428
+ */
429
+ description?: string;
430
+ /**
431
+ * Image URL of the product
432
+ */
433
+ imageUrl?: string;
434
+ /**
435
+ * Database ID of the line item
436
+ */
437
+ lineItemId?: string;
438
+ /**
439
+ * Name of the product
440
+ */
441
+ name: string;
442
+ /**
443
+ * Option values of the product variant
444
+ */
445
+ optionValues?: string[];
446
+ /**
447
+ * Position in the product list
448
+ */
449
+ position?: number;
450
+ /**
451
+ * Price ($) of the product
452
+ */
453
+ price: number;
454
+ /**
455
+ * Database ID of the product
456
+ */
457
+ productId: string;
458
+ /**
459
+ * Quantity of the product
460
+ */
461
+ quantity: number;
462
+ /**
463
+ * Revenue ($) with discounts and coupons added in
464
+ */
465
+ revenue: number;
466
+ /**
467
+ * SKU of the product variant
468
+ */
469
+ sku: string;
470
+ /**
471
+ * Slug of the product
472
+ */
473
+ slug?: string;
474
+ /**
475
+ * URL of the product page
476
+ */
477
+ url?: string;
478
+ /**
479
+ * Name of the product variant
480
+ */
481
+ variant?: string;
482
+ /**
483
+ * Metadata of the product variant
484
+ */
485
+ variantMetadata?: {
486
+ [key: string]: any;
487
+ };
488
+ [property: string]: any;
489
+ }
490
+ export interface OrderCompletedShippingMethodDetailsInput {
491
+ /**
492
+ * Code of shipping method (for example, "standard_shipping")
493
+ */
494
+ shippingCode?: string;
495
+ /**
496
+ * Name of shipping method (for example, "Standard Shipping")
497
+ */
498
+ shippingMethod?: string;
499
+ }
500
+ export interface OrderCompletedInput {
501
+ /**
502
+ * Store or affiliation from which this transaction occurred
503
+ */
504
+ affiliation?: string;
505
+ /**
506
+ * Order metadata "campaign_id"
507
+ */
508
+ campaignId?: number;
509
+ /**
510
+ * Order category
511
+ */
512
+ category?: string;
513
+ /**
514
+ * Channel where order was placed (for example, "chord")
515
+ */
516
+ channel?: string;
517
+ /**
518
+ * Checkout ID
519
+ */
520
+ checkoutId?: string;
521
+ /**
522
+ * Checkout provider (for example, "stripe")
523
+ */
524
+ checkoutType?: string;
525
+ /**
526
+ * Transaction coupon redeemed with the transaction
527
+ */
528
+ coupon?: string;
529
+ /**
530
+ * Currency code of the transaction (for example, "USD")
531
+ */
532
+ currency: string;
533
+ /**
534
+ * Total discount associated with the transaction
535
+ */
536
+ discount?: number;
537
+ /**
538
+ * Email address associated with the order
539
+ */
540
+ email?: string;
541
+ /**
542
+ * First name in the shipping address for the order
543
+ */
544
+ firstName?: string;
545
+ /**
546
+ * Order contains a subscription
547
+ */
548
+ hasSubscription?: boolean;
549
+ /**
550
+ * Order is a recurring subscription order
551
+ */
552
+ isRecurring?: boolean;
553
+ /**
554
+ * Last name in the shipping address for the order
555
+ */
556
+ lastName?: string;
557
+ /**
558
+ * Metadata of the order
559
+ */
560
+ metadata?: {
561
+ [key: string]: any;
562
+ };
563
+ /**
564
+ * Date and time order was placed
565
+ */
566
+ orderDate: string;
567
+ /**
568
+ * Order/transaction ID
569
+ */
570
+ orderId: string;
571
+ /**
572
+ * Phone number associated with the order
573
+ */
574
+ phone?: string;
575
+ /**
576
+ * Products in the order
577
+ */
578
+ products: OrderCompletedProductInput[];
579
+ /**
580
+ * URL for customer to share to receive referral credit
581
+ */
582
+ purlLink?: string;
583
+ /**
584
+ * ID for referral that customer used to place the order
585
+ */
586
+ referralIdentifierId?: string;
587
+ /**
588
+ * Indicates whether the customer is new or returning
589
+ */
590
+ repeat?: boolean;
591
+ /**
592
+ * Revenue ($) associated with the transaction (excluding shipping and tax)
593
+ */
594
+ revenue: number;
595
+ /**
596
+ * Shipping cost associated with the transaction
597
+ */
598
+ shipping?: number;
599
+ /**
600
+ * City in the shipping address for the order
601
+ */
602
+ shippingCity?: string;
603
+ /**
604
+ * Country in the shipping address for the order
605
+ */
606
+ shippingCountry?: string;
607
+ /**
608
+ * Shipping method details
609
+ */
610
+ shippingMethodDetails?: OrderCompletedShippingMethodDetailsInput;
611
+ /**
612
+ * State in the shipping address for the order
613
+ */
614
+ shippingState?: string;
615
+ /**
616
+ * Zipcode in the shipping address for the order
617
+ */
618
+ shippingZipcode?: string;
619
+ /**
620
+ * Order total after discounts but before taxes and shipping
621
+ */
622
+ subtotal?: number;
623
+ /**
624
+ * Total tax associated with the transaction
625
+ */
626
+ tax?: number;
627
+ /**
628
+ * Order metadata "template_id"
629
+ */
630
+ templateId?: number;
631
+ /**
632
+ * Revenue ($) with discounts and coupons added in
633
+ */
634
+ total: number;
635
+ /**
636
+ * Tracking IDs for third party integrations
637
+ */
638
+ trackingIds?: {
639
+ [key: string]: any;
640
+ };
641
+ }
@@ -1,4 +1,4 @@
1
- import type { AnyAnalytics, EventProperties, Options, ValidateResult, ChordAnalyticsOptions, AnyOptions, CartViewedInput, IdentifyTraits, CheckoutStartedInput, CheckoutStepCompletedInput, CheckoutStepViewedInput, EmailCapturedInput, ProductListViewedInput, ProductListFilteredInput, ProductAddedInput, ProductClickedInput, VariantClickedInput, ProductRemovedInput, ProductViewedInput, ProductsSearchedInput, CouponAppliedInput, CouponDeniedInput, CouponEnteredInput, CouponRemovedInput, SignedInInput, SignedOutInput, SignedUpInput, LoginStartedInput, ObjectTypes, SubscriptionCancelledInput, NavigationClickedInput, PaymentInfoEnteredInput } from '../types/index.js';
1
+ import type { AnyAnalytics, EventProperties, Options, ValidateResult, ChordAnalyticsOptions, AnyOptions, CartViewedInput, IdentifyTraits, CheckoutStartedInput, CheckoutStepCompletedInput, CheckoutStepViewedInput, EmailCapturedInput, ProductListViewedInput, ProductListFilteredInput, ProductAddedInput, ProductClickedInput, VariantClickedInput, ProductRemovedInput, ProductViewedInput, ProductsSearchedInput, CouponAppliedInput, CouponDeniedInput, CouponEnteredInput, CouponRemovedInput, SignedInInput, SignedOutInput, SignedUpInput, LoginStartedInput, ObjectTypes, OrderCompletedInput, SubscriptionCancelledInput, NavigationClickedInput, PaymentInfoEnteredInput } from '../types/index.js';
2
2
  import { AnalyticsMiddleware } from '../types/client.js';
3
3
  export interface ChordAnalyticsSnippet {
4
4
  _loadOptions: ChordAnalyticsOptions<ObjectTypes>;
@@ -45,6 +45,20 @@ export declare class ChordAnalytics<T extends ObjectTypes = ObjectTypes> {
45
45
  */
46
46
  private consentReadyPromise;
47
47
  constructor(options: ChordAnalyticsOptions<T>);
48
+ /**
49
+ * Persist the consent event queue to sessionStorage for recovery on instance recreation.
50
+ * Validates queue size before storing to prevent QuotaExceededError.
51
+ */
52
+ private persistQueueToStorage;
53
+ /**
54
+ * Restore pending events from sessionStorage (keeps events less than 5 min old).
55
+ * Called on new instance initialization.
56
+ */
57
+ private restoreQueueFromStorage;
58
+ /**
59
+ * Clear the persisted queue from sessionStorage after successful flush.
60
+ */
61
+ private clearQueueFromStorage;
48
62
  /**
49
63
  * Initialize CDP snippet and wait for it to be ready.
50
64
  * Fires initial consent track event after CDP is fully initialized to avoid circular await.
@@ -66,6 +80,7 @@ export declare class ChordAnalytics<T extends ObjectTypes = ObjectTypes> {
66
80
  private waitForConsent;
67
81
  /**
68
82
  * Queue an event to be sent when consent is ready.
83
+ * Persists to sessionStorage for recovery on instance recreation.
69
84
  * Starts a consent watcher if not already active.
70
85
  */
71
86
  private queueEventForConsent;
@@ -75,6 +90,7 @@ export declare class ChordAnalytics<T extends ObjectTypes = ObjectTypes> {
75
90
  private watchForConsentAndFlush;
76
91
  /**
77
92
  * Flush all queued events now that consent is ready.
93
+ * Clears sessionStorage after successful flush.
78
94
  */
79
95
  private flushConsentQueue;
80
96
  /**
@@ -159,4 +175,5 @@ export declare class ChordAnalytics<T extends ObjectTypes = ObjectTypes> {
159
175
  trackSubscriptionCancelled: (props: SubscriptionCancelledInput, options?: AnyOptions) => Promise<void>;
160
176
  trackNavigationClicked: (props: NavigationClickedInput, options?: AnyOptions) => Promise<void>;
161
177
  trackPaymentInfoEntered: (props: PaymentInfoEnteredInput, options?: AnyOptions) => Promise<void>;
178
+ trackOrderCompleted: (props: OrderCompletedInput, options?: AnyOptions) => Promise<void>;
162
179
  }
@@ -12,6 +12,10 @@ import { addCdpConsent } from './consent/index.js';
12
12
  import { eventSchemas } from '../validators/index.js';
13
13
  import { pruneNullValues } from '../utils.js';
14
14
  const DEBUG_MODE_STORAGE_KEY = 'chordcommerce/analytics/debug';
15
+ const CONSENT_QUEUE_STORAGE_KEY = 'chordcommerce/analytics/consent-queue';
16
+ const CONSENT_QUEUE_TTL_MS = 5 * 60 * 1000; // 5 minutes
17
+ const CONSENT_QUEUE_MAX_SIZE_BYTES = 100 * 1024; // 100KB max queue size
18
+ const CONSENT_QUEUE_MAX_EVENTS = 50; // Max number of events to queue
15
19
  export class ChordAnalytics {
16
20
  constructor(options) {
17
21
  var _a, _b, _c;
@@ -45,6 +49,123 @@ export class ChordAnalytics {
45
49
  * Promise that resolves when consent is ready (shared across all queued events)
46
50
  */
47
51
  this.consentReadyPromise = null;
52
+ /**
53
+ * Persist the consent event queue to sessionStorage for recovery on instance recreation.
54
+ * Validates queue size before storing to prevent QuotaExceededError.
55
+ */
56
+ this.persistQueueToStorage = () => {
57
+ if (typeof window === 'undefined' || !window.sessionStorage)
58
+ return;
59
+ try {
60
+ // Limit number of events to prevent unbounded queue growth
61
+ const eventsToStore = this.consentEventQueue.slice(-CONSENT_QUEUE_MAX_EVENTS);
62
+ const persistedEvents = eventsToStore.map((item) => ({
63
+ type: item.type,
64
+ args: item.args,
65
+ timestamp: item.timestamp,
66
+ }));
67
+ const serialized = JSON.stringify(persistedEvents);
68
+ // Check serialized size before attempting to store
69
+ const sizeInBytes = new Blob([serialized]).size;
70
+ if (sizeInBytes > CONSENT_QUEUE_MAX_SIZE_BYTES) {
71
+ this.logger(`[Chord Analytics]: Consent queue too large (${Math.round(sizeInBytes / 1024)}KB), skipping persistence`);
72
+ return;
73
+ }
74
+ window.sessionStorage.setItem(CONSENT_QUEUE_STORAGE_KEY, serialized);
75
+ }
76
+ catch (error) {
77
+ this.logger('[Chord Analytics]: Error persisting queue to storage:', error);
78
+ }
79
+ };
80
+ /**
81
+ * Restore pending events from sessionStorage (keeps events less than 5 min old).
82
+ * Called on new instance initialization.
83
+ */
84
+ this.restoreQueueFromStorage = () => {
85
+ if (typeof window === 'undefined' || !window.sessionStorage)
86
+ return;
87
+ try {
88
+ const stored = window.sessionStorage.getItem(CONSENT_QUEUE_STORAGE_KEY);
89
+ if (!stored)
90
+ return;
91
+ let persistedEvents;
92
+ try {
93
+ persistedEvents = JSON.parse(stored);
94
+ }
95
+ catch (parseError) {
96
+ this.logger('[Chord Analytics]: Invalid JSON in stored consent queue, clearing storage:', parseError);
97
+ window.sessionStorage.removeItem(CONSENT_QUEUE_STORAGE_KEY);
98
+ return;
99
+ }
100
+ // Validate that parsed data is an array
101
+ if (!Array.isArray(persistedEvents)) {
102
+ this.logger('[Chord Analytics]: Stored consent queue is not an array, clearing storage');
103
+ window.sessionStorage.removeItem(CONSENT_QUEUE_STORAGE_KEY);
104
+ return;
105
+ }
106
+ const now = Date.now();
107
+ // Filter out expired events and events with invalid structure
108
+ const validEvents = persistedEvents.filter((event) => {
109
+ // Validate event structure
110
+ if (typeof event !== 'object' || event === null)
111
+ return false;
112
+ if (!['track', 'page', 'identify'].includes(event.type))
113
+ return false;
114
+ if (!Array.isArray(event.args))
115
+ return false;
116
+ if (typeof event.timestamp !== 'number')
117
+ return false;
118
+ // Filter out expired events
119
+ return now - event.timestamp < CONSENT_QUEUE_TTL_MS;
120
+ });
121
+ if (validEvents.length === 0) {
122
+ window.sessionStorage.removeItem(CONSENT_QUEUE_STORAGE_KEY);
123
+ return;
124
+ }
125
+ if (this.options.enableLogging && this.options.debug) {
126
+ // eslint-disable-next-line no-console
127
+ console.log(`[Chord Analytics DEBUG]: Restoring ${validEvents.length} events from sessionStorage`);
128
+ }
129
+ // Add restored events to the queue
130
+ validEvents.forEach((event) => {
131
+ this.consentEventQueue.push({
132
+ type: event.type,
133
+ args: event.args,
134
+ timestamp: event.timestamp,
135
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
136
+ resolve: () => { },
137
+ });
138
+ });
139
+ // Start watching for consent if we restored events
140
+ if (validEvents.length > 0 && !this.consentWatcherActive) {
141
+ this.consentWatcherActive = true;
142
+ this.watchForConsentAndFlush();
143
+ }
144
+ }
145
+ catch (error) {
146
+ this.logger('[Chord Analytics]: Error restoring queue from storage:', error);
147
+ // Clear corrupted storage
148
+ try {
149
+ window.sessionStorage.removeItem(CONSENT_QUEUE_STORAGE_KEY);
150
+ }
151
+ catch (_a) {
152
+ // Ignore cleanup errors
153
+ }
154
+ }
155
+ };
156
+ /**
157
+ * Clear the persisted queue from sessionStorage after successful flush.
158
+ */
159
+ this.clearQueueFromStorage = () => {
160
+ if (typeof window === 'undefined' || !window.sessionStorage)
161
+ return;
162
+ try {
163
+ window.sessionStorage.removeItem(CONSENT_QUEUE_STORAGE_KEY);
164
+ }
165
+ catch (error) {
166
+ this.logger('[Chord Analytics]: Error clearing queue from storage:', error);
167
+ }
168
+ };
48
169
  /**
49
170
  * Set debug mode for Chord Analytics
50
171
  * @param enabled - Whether debug mode should be enabled
@@ -115,11 +236,19 @@ export class ChordAnalytics {
115
236
  });
116
237
  /**
117
238
  * Queue an event to be sent when consent is ready.
239
+ * Persists to sessionStorage for recovery on instance recreation.
118
240
  * Starts a consent watcher if not already active.
119
241
  */
120
242
  this.queueEventForConsent = (type, args) => {
121
243
  return new Promise((resolve) => {
122
- this.consentEventQueue.push({ type, args, resolve });
244
+ this.consentEventQueue.push({
245
+ type,
246
+ args,
247
+ timestamp: Date.now(),
248
+ resolve,
249
+ });
250
+ // Persist queue to sessionStorage for recovery on instance recreation
251
+ this.persistQueueToStorage();
123
252
  // Start watching for consent if not already
124
253
  if (!this.consentWatcherActive) {
125
254
  this.consentWatcherActive = true;
@@ -137,10 +266,17 @@ export class ChordAnalytics {
137
266
  });
138
267
  /**
139
268
  * Flush all queued events now that consent is ready.
269
+ * Clears sessionStorage after successful flush.
140
270
  */
141
271
  this.flushConsentQueue = () => __awaiter(this, void 0, void 0, function* () {
142
272
  const queue = [...this.consentEventQueue];
143
273
  this.consentEventQueue = [];
274
+ // Clear persisted queue from storage since we're processing them now
275
+ this.clearQueueFromStorage();
276
+ if (this.options.enableLogging && this.options.debug) {
277
+ // eslint-disable-next-line no-console
278
+ console.log(`[Chord Analytics DEBUG]: Flushing ${queue.length} queued events`);
279
+ }
144
280
  const sendPromises = queue.map((item) => __awaiter(this, void 0, void 0, function* () {
145
281
  try {
146
282
  switch (item.type) {
@@ -313,12 +449,7 @@ export class ChordAnalytics {
313
449
  }));
314
450
  }
315
451
  if (cdpPromises.length > 0) {
316
- Promise.allSettled(cdpPromises)
317
- .then(() => resolve())
318
- .catch((error) => {
319
- this.logger(error);
320
- resolve();
321
- });
452
+ Promise.allSettled(cdpPromises).then(() => resolve());
322
453
  }
323
454
  else {
324
455
  resolve();
@@ -422,28 +553,36 @@ export class ChordAnalytics {
422
553
  this.sendPageEvent = (options, props = {}) => __awaiter(this, void 0, void 0, function* () {
423
554
  // Configure CDP with fresh consent before sending
424
555
  this.configureConsentOnCdp();
425
- try {
556
+ yield new Promise((resolve) => {
426
557
  const pagePromises = [];
427
- const a = this.ccdp();
428
- if (a && typeof a.page === 'function') {
429
- pagePromises.push(new Promise((resolve) => {
430
- a.page(Object.assign(Object.assign({}, props), { meta: this.meta() }), options, () => resolve());
431
- }));
558
+ try {
559
+ const a = this.ccdp();
560
+ if (a && typeof a.page === 'function') {
561
+ pagePromises.push(new Promise((cdpResolve) => {
562
+ a.page(Object.assign(Object.assign({}, props), { meta: this.meta() }), options, () => cdpResolve());
563
+ }));
564
+ }
565
+ const b = this.cdp();
566
+ if (b && typeof b.page === 'function') {
567
+ pagePromises.push(new Promise((cdpResolve) => {
568
+ b.page(Object.assign(Object.assign({}, props), { meta: this.meta() }), options, () => cdpResolve());
569
+ }));
570
+ }
571
+ if (pagePromises.length > 0) {
572
+ Promise.allSettled(pagePromises).then((results) => {
573
+ this.logger('page event sent', results);
574
+ resolve();
575
+ });
576
+ }
577
+ else {
578
+ resolve();
579
+ }
432
580
  }
433
- const b = this.cdp();
434
- if (b && typeof b.page === 'function') {
435
- pagePromises.push(new Promise((resolve) => {
436
- b.page(Object.assign(Object.assign({}, props), { meta: this.meta() }), options, () => resolve());
437
- }));
581
+ catch (error) {
582
+ this.logger(error);
583
+ resolve();
438
584
  }
439
- // We don't return the promise to maintain the void return type
440
- Promise.allSettled(pagePromises).then((results) => {
441
- this.logger('page event sent', results);
442
- });
443
- }
444
- catch (error) {
445
- this.logger(error);
446
- }
585
+ });
447
586
  });
448
587
  this.reset = () => __awaiter(this, void 0, void 0, function* () {
449
588
  var _d;
@@ -871,6 +1010,82 @@ export class ChordAnalytics {
871
1010
  }
872
1011
  return this.track('Payment Info Entered', payload, options);
873
1012
  });
1013
+ this.trackOrderCompleted = (props, options) => __awaiter(this, void 0, void 0, function* () {
1014
+ var _122, _123, _124, _125;
1015
+ const formatter = (_124 = (_123 = (_122 = this.options) === null || _122 === void 0 ? void 0 : _122.formatters) === null || _123 === void 0 ? void 0 : _123.events) === null || _124 === void 0 ? void 0 : _124.orderCompleted;
1016
+ const payload = {
1017
+ affiliation: props === null || props === void 0 ? void 0 : props.affiliation,
1018
+ campaign_id: props === null || props === void 0 ? void 0 : props.campaignId,
1019
+ category: props === null || props === void 0 ? void 0 : props.category,
1020
+ channel: props === null || props === void 0 ? void 0 : props.channel,
1021
+ checkout_id: props === null || props === void 0 ? void 0 : props.checkoutId,
1022
+ checkout_type: props === null || props === void 0 ? void 0 : props.checkoutType,
1023
+ coupon: props === null || props === void 0 ? void 0 : props.coupon,
1024
+ currency: props === null || props === void 0 ? void 0 : props.currency,
1025
+ discount: props === null || props === void 0 ? void 0 : props.discount,
1026
+ email: props === null || props === void 0 ? void 0 : props.email,
1027
+ first_name: props === null || props === void 0 ? void 0 : props.firstName,
1028
+ has_subscription: props === null || props === void 0 ? void 0 : props.hasSubscription,
1029
+ is_recurring: props === null || props === void 0 ? void 0 : props.isRecurring,
1030
+ last_name: props === null || props === void 0 ? void 0 : props.lastName,
1031
+ metadata: props === null || props === void 0 ? void 0 : props.metadata,
1032
+ order_date: props === null || props === void 0 ? void 0 : props.orderDate,
1033
+ order_id: props === null || props === void 0 ? void 0 : props.orderId,
1034
+ phone: props === null || props === void 0 ? void 0 : props.phone,
1035
+ products: (_125 = props === null || props === void 0 ? void 0 : props.products) === null || _125 === void 0 ? void 0 : _125.map((p, i) => {
1036
+ var _a;
1037
+ return ({
1038
+ affiliation: p === null || p === void 0 ? void 0 : p.affiliation,
1039
+ brand: p === null || p === void 0 ? void 0 : p.brand,
1040
+ bundle: p === null || p === void 0 ? void 0 : p.bundle,
1041
+ bundle_size: p === null || p === void 0 ? void 0 : p.bundleSize,
1042
+ category: p === null || p === void 0 ? void 0 : p.category,
1043
+ coupon: p === null || p === void 0 ? void 0 : p.coupon,
1044
+ cross_sold_quantity: p === null || p === void 0 ? void 0 : p.crossSoldQuantity,
1045
+ description: p === null || p === void 0 ? void 0 : p.description,
1046
+ image_url: p === null || p === void 0 ? void 0 : p.imageUrl,
1047
+ line_item_id: p === null || p === void 0 ? void 0 : p.lineItemId,
1048
+ name: p === null || p === void 0 ? void 0 : p.name,
1049
+ option_values: p === null || p === void 0 ? void 0 : p.optionValues,
1050
+ position: (_a = p === null || p === void 0 ? void 0 : p.position) !== null && _a !== void 0 ? _a : i + 1,
1051
+ price: p === null || p === void 0 ? void 0 : p.price,
1052
+ product_id: p === null || p === void 0 ? void 0 : p.productId,
1053
+ quantity: p === null || p === void 0 ? void 0 : p.quantity,
1054
+ revenue: p === null || p === void 0 ? void 0 : p.revenue,
1055
+ sku: p === null || p === void 0 ? void 0 : p.sku,
1056
+ slug: p === null || p === void 0 ? void 0 : p.slug,
1057
+ url: p === null || p === void 0 ? void 0 : p.url,
1058
+ variant: p === null || p === void 0 ? void 0 : p.variant,
1059
+ variant_metadata: p === null || p === void 0 ? void 0 : p.variantMetadata,
1060
+ });
1061
+ }),
1062
+ purl_link: props === null || props === void 0 ? void 0 : props.purlLink,
1063
+ referral_identifier_id: props === null || props === void 0 ? void 0 : props.referralIdentifierId,
1064
+ repeat: props === null || props === void 0 ? void 0 : props.repeat,
1065
+ revenue: props === null || props === void 0 ? void 0 : props.revenue,
1066
+ shipping: props === null || props === void 0 ? void 0 : props.shipping,
1067
+ shipping_city: props === null || props === void 0 ? void 0 : props.shippingCity,
1068
+ shipping_country: props === null || props === void 0 ? void 0 : props.shippingCountry,
1069
+ shipping_method_details: (props === null || props === void 0 ? void 0 : props.shippingMethodDetails)
1070
+ ? {
1071
+ shipping_code: props.shippingMethodDetails.shippingCode,
1072
+ shipping_method: props.shippingMethodDetails.shippingMethod,
1073
+ }
1074
+ : undefined,
1075
+ shipping_state: props === null || props === void 0 ? void 0 : props.shippingState,
1076
+ shipping_zipcode: props === null || props === void 0 ? void 0 : props.shippingZipcode,
1077
+ subtotal: props === null || props === void 0 ? void 0 : props.subtotal,
1078
+ tax: props === null || props === void 0 ? void 0 : props.tax,
1079
+ template_id: props === null || props === void 0 ? void 0 : props.templateId,
1080
+ total: props === null || props === void 0 ? void 0 : props.total,
1081
+ tracking_ids: props === null || props === void 0 ? void 0 : props.trackingIds,
1082
+ };
1083
+ if (typeof formatter === 'function') {
1084
+ const formattedPayload = formatter(props, payload);
1085
+ return this.track('Order Completed', formattedPayload, options);
1086
+ }
1087
+ return this.track('Order Completed', payload, options);
1088
+ });
874
1089
  const debug = ((_a = window === null || window === void 0 ? void 0 : window.localStorage) === null || _a === void 0 ? void 0 : _a.getItem(DEBUG_MODE_STORAGE_KEY)) === 'true';
875
1090
  const defaults = {
876
1091
  debug,
@@ -883,6 +1098,8 @@ export class ChordAnalytics {
883
1098
  const consentManager = addCdpConsent(this.options);
884
1099
  this.consentConfig = consentManager.getConsentConfig() || null;
885
1100
  }
1101
+ // Restore any pending events from sessionStorage (survives instance recreation)
1102
+ this.restoreQueueFromStorage();
886
1103
  if (((_b = this.options) === null || _b === void 0 ? void 0 : _b.cdpDomain) && ((_c = this.options) === null || _c === void 0 ? void 0 : _c.cdpWriteKey) && window) {
887
1104
  window[this.options.namespace] = (window === null || window === void 0 ? void 0 : window[this.options.namespace]) || this;
888
1105
  this.initializeCdp();
@@ -2,3 +2,26 @@
2
2
  import { jest } from '@jest/globals';
3
3
  // Make jest available globally
4
4
  globalThis.jest = jest;
5
+ // Mock sessionStorage for tests
6
+ const sessionStorageMock = (() => {
7
+ let store = {};
8
+ return {
9
+ getItem: (key) => store[key] || null,
10
+ setItem: (key, value) => {
11
+ store[key] = value;
12
+ },
13
+ removeItem: (key) => {
14
+ delete store[key];
15
+ },
16
+ clear: () => {
17
+ store = {};
18
+ },
19
+ };
20
+ })();
21
+ Object.defineProperty(window, 'sessionStorage', {
22
+ value: sessionStorageMock,
23
+ });
24
+ // Clear sessionStorage before each test to ensure test isolation
25
+ beforeEach(() => {
26
+ sessionStorageMock.clear();
27
+ });