@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.
- package/dist/cjs/client/index.d.ts +18 -1
- package/dist/cjs/client/index.js +260 -32
- package/dist/cjs/setupTests.js +23 -0
- package/dist/cjs/types/formatters.d.ts +6 -2
- package/dist/cjs/types/input.d.ts +245 -0
- package/dist/client/index.d.ts +18 -1
- package/dist/client/index.js +243 -26
- package/dist/setupTests.js +23 -0
- package/dist/types/formatters.d.ts +6 -2
- package/dist/types/input.d.ts +245 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/client/index.js
CHANGED
|
@@ -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({
|
|
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
|
-
|
|
556
|
+
yield new Promise((resolve) => {
|
|
426
557
|
const pagePromises = [];
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
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();
|
package/dist/setupTests.js
CHANGED
|
@@ -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
|
+
});
|