@chordcommerce/analytics 1.21.1 → 1.22.0-beta.1

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/README.md CHANGED
@@ -168,15 +168,28 @@ This package uses [semantic-release](https://github.com/semantic-release/semanti
168
168
 
169
169
  ### How it works
170
170
 
171
+ The repo has a single CI workflow (`.github/workflows/ci.yml`) with two jobs:
172
+
173
+ 1. **`test`** — Runs lint, prettier, build, and tests for all packages. Triggers on every push to `main`/`beta` and on every pull request.
174
+ 2. **`release`** — Runs only on `main` and `beta` branches, after `test` passes. Runs `semantic-release` to determine if a new version should be published.
175
+
176
+ The release flow:
177
+
171
178
  1. Merge a PR into `main` (stable) or `beta` (pre-release).
172
- 2. The **Test** GitHub Actions workflow runs.
173
- 3. On success, the **Semantic Release** workflow runs automatically.
174
- 4. `semantic-release` analyzes commit messages since the last release and determines the version bump.
175
- 5. The package is published to npm and GitHub Packages, CHANGELOG.md is updated, and a Git tag is created.
179
+ 2. The `test` job runs. If it passes, the `release` job starts.
180
+ 3. `semantic-release` analyzes commit messages since the last release tag.
181
+ 4. If releasable commits exist, the package is published to npm and GitHub Packages, CHANGELOG.md is updated, and a Git tag is created.
182
+ 5. If no releasable commits exist, `semantic-release` exits cleanly and nothing is published.
176
183
 
177
- ### Commit message conventions
184
+ ### When a release happens (and when it doesn't)
178
185
 
179
- This repo enforces [Conventional Commits](https://www.conventionalcommits.org/) via commitlint. The commit message prefix determines the version bump:
186
+ A release is published **only** when all three conditions are met:
187
+
188
+ 1. A push lands on `main` or `beta` (via merged PR or direct push)
189
+ 2. Tests pass
190
+ 3. There are new commits since the last release tag with a releasable prefix
191
+
192
+ **Releasable commit prefixes** (these trigger a version bump):
180
193
 
181
194
  | Prefix | Version Bump | Example |
182
195
  | -------------------------- | --------------------- | --------------------------------------------------------- |
@@ -184,19 +197,35 @@ This repo enforces [Conventional Commits](https://www.conventionalcommits.org/)
184
197
  | `feat:` | Minor (1.0.0 → 1.1.0) | `feat: add trackOrderCompleted method` |
185
198
  | `BREAKING CHANGE:` in body | Major (1.0.0 → 2.0.0) | Commit body contains `BREAKING CHANGE: remove legacy API` |
186
199
 
187
- Other prefixes (`chore:`, `ci:`, `docs:`, `refactor:`, `test:`, etc.) do **not** trigger a release.
200
+ **Non-releasable prefixes** (these do NOT trigger a release):
201
+
202
+ `chore:`, `ci:`, `docs:`, `refactor:`, `test:`, `style:`, `build:`, `perf:`
203
+
204
+ If a merge to `main` or `beta` contains only non-releasable commits, the release job runs but `semantic-release` determines no release is needed and exits without publishing.
188
205
 
189
206
  ### Monorepo scoping
190
207
 
191
- This repo uses [semantic-release-monorepo](https://github.com/pmowrer/semantic-release-monorepo) to scope releases to individual packages. Only commits that touch files within a package's directory (e.g., `packages/core/`) are considered when determining the next version. Commits that only modify root-level files like `.github/workflows/`, `.nvmrc`, or other packages will not trigger a release for this package.
208
+ This repo uses [semantic-release-monorepo](https://github.com/pmowrer/semantic-release-monorepo) to scope releases to individual packages. Only commits that touch files within a package's directory (e.g., `packages/core/`) are considered when determining the next version. A `feat:` commit that only modifies `.github/workflows/` or another package's directory will **not** trigger a release for `@chordcommerce/analytics`.
192
209
 
193
210
  ### Pre-releases
194
211
 
195
- Merging to `beta` creates pre-release versions (e.g., `1.21.0-beta.1`). Use this for testing before promoting to `main`.
212
+ Merging to `beta` creates pre-release versions (e.g., `1.22.0-beta.1`). Use this for testing before promoting to `main`. Only `main` and `beta` are currently configured for automatic releases in the CI workflow. Additional prerelease branches (e.g., `alpha`) can be enabled by adding them to both the workflow's branch filter in `ci.yml` and the semantic-release branch config.
213
+
214
+ ### Beta release workflow
215
+
216
+ 1. Create a feature branch off `beta` and do your work.
217
+ 2. Create a PR targeting the `beta` branch and merge it.
218
+ 3. The CI workflow runs tests, then `semantic-release` publishes a beta version (e.g., `1.22.0-beta.1`).
219
+ 4. Install with: `npm install @chordcommerce/analytics@beta`
220
+
221
+ ### Promoting beta to production
222
+
223
+ 1. Create a PR from `beta` → `main` and merge it.
224
+ 2. The CI workflow runs tests, then `semantic-release` publishes a stable version on `main`.
196
225
 
197
226
  ### Dry run
198
227
 
199
- You can preview what a release would do without publishing by manually triggering the Semantic Release workflow in GitHub Actions with `dryRun` set to `true`.
228
+ You can preview what a release would do without publishing by manually triggering the CI workflow in GitHub Actions with `dryRun` set to `true`.
200
229
 
201
230
  ## Support
202
231
 
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Shopify Identifier Sync
3
+ *
4
+ * Syncs CDP anonymous ID and Google Client ID to Shopify cart attributes.
5
+ * This enables server-side events (webhooks) to correlate with client-side tracking.
6
+ */
7
+ export interface ShopifyCart {
8
+ attributes?: Record<string, unknown>;
9
+ [key: string]: unknown;
10
+ }
11
+ export interface IdentifierSyncOptions {
12
+ /**
13
+ * Base URL for Shopify AJAX API calls. Defaults to window.location.origin
14
+ */
15
+ baseUrl?: string;
16
+ /**
17
+ * Locale path for Shopify routes. Defaults to window.Shopify.routes.root or '/'
18
+ */
19
+ localePath?: string;
20
+ /**
21
+ * Custom identifier getters. By default, reads CDP anonymous ID and Google Client ID.
22
+ */
23
+ identifiers?: () => Record<string, string>;
24
+ }
25
+ /**
26
+ * Get a cookie value by name
27
+ */
28
+ export declare const getCookie: (name: string) => string | undefined;
29
+ /**
30
+ * Get default identifiers (CDP anonymous ID and Google Client ID)
31
+ */
32
+ export declare const getDefaultIdentifiers: () => Record<string, string>;
33
+ /**
34
+ * Fetch the current Shopify cart
35
+ */
36
+ export declare const getShopifyCart: (baseUrl?: string, localePath?: string) => Promise<ShopifyCart>;
37
+ /**
38
+ * Update Shopify cart attributes
39
+ */
40
+ export declare const updateShopifyCartAttributes: (attributes: Record<string, string>, baseUrl?: string, localePath?: string) => Promise<ShopifyCart>;
41
+ /**
42
+ * Sync identifiers to Shopify cart attributes.
43
+ *
44
+ * This function:
45
+ * 1. Gets the current cart
46
+ * 2. Checks if identifiers already exist
47
+ * 3. If not, updates the cart with the identifiers
48
+ *
49
+ * @returns The updated cart, or undefined if no update was needed
50
+ */
51
+ export declare const syncIdentifiersToCart: (options?: IdentifierSyncOptions) => Promise<ShopifyCart | undefined>;
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ /**
3
+ * Shopify Identifier Sync
4
+ *
5
+ * Syncs CDP anonymous ID and Google Client ID to Shopify cart attributes.
6
+ * This enables server-side events (webhooks) to correlate with client-side tracking.
7
+ */
8
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
9
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
10
+ return new (P || (P = Promise))(function (resolve, reject) {
11
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
12
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
13
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
14
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
15
+ });
16
+ };
17
+ var __generator = (this && this.__generator) || function (thisArg, body) {
18
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
19
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
20
+ function verb(n) { return function (v) { return step([n, v]); }; }
21
+ function step(op) {
22
+ if (f) throw new TypeError("Generator is already executing.");
23
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
24
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
25
+ if (y = 0, t) op = [op[0] & 2, t.value];
26
+ switch (op[0]) {
27
+ case 0: case 1: t = op; break;
28
+ case 4: _.label++; return { value: op[1], done: false };
29
+ case 5: _.label++; y = op[1]; op = [0]; continue;
30
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
31
+ default:
32
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
33
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
34
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
35
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
36
+ if (t[2]) _.ops.pop();
37
+ _.trys.pop(); continue;
38
+ }
39
+ op = body.call(thisArg, _);
40
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
41
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
42
+ }
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.syncIdentifiersToCart = exports.updateShopifyCartAttributes = exports.getShopifyCart = exports.getDefaultIdentifiers = exports.getCookie = void 0;
46
+ /**
47
+ * Get a cookie value by name
48
+ */
49
+ var getCookie = function (name) {
50
+ var _a;
51
+ if (typeof document === 'undefined')
52
+ return undefined;
53
+ return (_a = document.cookie
54
+ .split('; ')
55
+ .find(function (row) { return row.startsWith("".concat(name, "=")); })) === null || _a === void 0 ? void 0 : _a.split('=')[1];
56
+ };
57
+ exports.getCookie = getCookie;
58
+ /**
59
+ * Get default identifiers (CDP anonymous ID and Google Client ID)
60
+ */
61
+ var getDefaultIdentifiers = function () {
62
+ var attributes = {};
63
+ try {
64
+ // CDP anonymous ID (Jitsu/Chord CDP)
65
+ var anonymousId = (0, exports.getCookie)('__eventn_id');
66
+ if (anonymousId) {
67
+ // eslint-disable-next-line no-underscore-dangle
68
+ attributes._cdpAnonymousId = anonymousId;
69
+ }
70
+ // Google Client ID (strip the "GA1.1." or similar prefix)
71
+ var gaCookie = (0, exports.getCookie)('_ga');
72
+ if (gaCookie) {
73
+ var googleClientId = gaCookie.substring(6);
74
+ if (googleClientId) {
75
+ // eslint-disable-next-line no-underscore-dangle
76
+ attributes._googleClientId = googleClientId;
77
+ }
78
+ }
79
+ }
80
+ catch (e) {
81
+ // eslint-disable-next-line no-console
82
+ console.error('[Chord Analytics]: Error getting identifiers:', e);
83
+ }
84
+ return attributes;
85
+ };
86
+ exports.getDefaultIdentifiers = getDefaultIdentifiers;
87
+ /**
88
+ * Construct a URL for Shopify AJAX API calls
89
+ */
90
+ var constructUrl = function (path, baseUrl, localePath) {
91
+ var _a, _b;
92
+ var defaultBaseUrl = typeof window !== 'undefined' ? (_a = window.location) === null || _a === void 0 ? void 0 : _a.origin : undefined;
93
+ // Access Shopify.routes.root for locale-aware URLs (cast needed as Window.Shopify type is partial)
94
+ var shopify = window === null || window === void 0 ? void 0 : window.Shopify;
95
+ var defaultLocalePath = ((_b = shopify === null || shopify === void 0 ? void 0 : shopify.routes) === null || _b === void 0 ? void 0 : _b.root) || '/';
96
+ var resolvedBaseUrl = baseUrl !== null && baseUrl !== void 0 ? baseUrl : defaultBaseUrl;
97
+ var resolvedLocalePath = localePath !== null && localePath !== void 0 ? localePath : defaultLocalePath;
98
+ var pathWithLocale = "".concat(resolvedLocalePath).concat(path);
99
+ if (!resolvedBaseUrl) {
100
+ return pathWithLocale.startsWith('/')
101
+ ? pathWithLocale
102
+ : "/".concat(pathWithLocale);
103
+ }
104
+ return new URL(pathWithLocale, resolvedBaseUrl);
105
+ };
106
+ /**
107
+ * Fetch the current Shopify cart
108
+ */
109
+ var getShopifyCart = function (baseUrl, localePath) { return __awaiter(void 0, void 0, void 0, function () {
110
+ var url, response;
111
+ return __generator(this, function (_a) {
112
+ switch (_a.label) {
113
+ case 0:
114
+ url = constructUrl('cart.js', baseUrl, localePath);
115
+ return [4 /*yield*/, fetch(url)];
116
+ case 1:
117
+ response = _a.sent();
118
+ if (!response.ok) {
119
+ throw new Error("Failed to fetch cart: ".concat(response.status));
120
+ }
121
+ return [2 /*return*/, response.json()];
122
+ }
123
+ });
124
+ }); };
125
+ exports.getShopifyCart = getShopifyCart;
126
+ /**
127
+ * Update Shopify cart attributes
128
+ */
129
+ var updateShopifyCartAttributes = function (attributes, baseUrl, localePath) { return __awaiter(void 0, void 0, void 0, function () {
130
+ var url, response;
131
+ return __generator(this, function (_a) {
132
+ switch (_a.label) {
133
+ case 0:
134
+ url = constructUrl('cart/update.js', baseUrl, localePath);
135
+ return [4 /*yield*/, fetch(url, {
136
+ method: 'POST',
137
+ headers: {
138
+ 'Content-Type': 'application/json',
139
+ },
140
+ body: JSON.stringify({ attributes: attributes }),
141
+ })];
142
+ case 1:
143
+ response = _a.sent();
144
+ if (!response.ok) {
145
+ throw new Error("Failed to update cart attributes: ".concat(response.status));
146
+ }
147
+ return [2 /*return*/, response.json()];
148
+ }
149
+ });
150
+ }); };
151
+ exports.updateShopifyCartAttributes = updateShopifyCartAttributes;
152
+ /**
153
+ * Sync identifiers to Shopify cart attributes.
154
+ *
155
+ * This function:
156
+ * 1. Gets the current cart
157
+ * 2. Checks if identifiers already exist
158
+ * 3. If not, updates the cart with the identifiers
159
+ *
160
+ * @returns The updated cart, or undefined if no update was needed
161
+ */
162
+ var syncIdentifiersToCart = function (options) {
163
+ if (options === void 0) { options = {}; }
164
+ return __awaiter(void 0, void 0, void 0, function () {
165
+ var baseUrl, localePath, _a, identifiers, cart_1, attributesToAdd_1, identifiersAlreadyExist, updatedCart, e_1;
166
+ return __generator(this, function (_b) {
167
+ switch (_b.label) {
168
+ case 0:
169
+ _b.trys.push([0, 3, , 4]);
170
+ baseUrl = options.baseUrl, localePath = options.localePath, _a = options.identifiers, identifiers = _a === void 0 ? exports.getDefaultIdentifiers : _a;
171
+ return [4 /*yield*/, (0, exports.getShopifyCart)(baseUrl, localePath)];
172
+ case 1:
173
+ cart_1 = _b.sent();
174
+ attributesToAdd_1 = identifiers();
175
+ identifiersAlreadyExist = Object.keys(attributesToAdd_1).every(function (key) { var _a; return ((_a = cart_1 === null || cart_1 === void 0 ? void 0 : cart_1.attributes) === null || _a === void 0 ? void 0 : _a[key]) === attributesToAdd_1[key]; });
176
+ if (identifiersAlreadyExist) {
177
+ return [2 /*return*/, undefined];
178
+ }
179
+ return [4 /*yield*/, (0, exports.updateShopifyCartAttributes)(attributesToAdd_1, baseUrl, localePath)];
180
+ case 2:
181
+ updatedCart = _b.sent();
182
+ return [2 /*return*/, updatedCart];
183
+ case 3:
184
+ e_1 = _b.sent();
185
+ // eslint-disable-next-line no-console
186
+ console.error('[Chord Analytics]: Error syncing identifiers to cart:', e_1);
187
+ return [2 /*return*/, undefined];
188
+ case 4: return [2 /*return*/];
189
+ }
190
+ });
191
+ });
192
+ };
193
+ exports.syncIdentifiersToCart = syncIdentifiersToCart;
@@ -1,5 +1,6 @@
1
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
+ import { type IdentifierSyncOptions, type ShopifyCart } from './identifiers.js';
3
4
  export interface ChordAnalyticsSnippet {
4
5
  _loadOptions: ChordAnalyticsOptions<ObjectTypes>;
5
6
  invoked: boolean;
@@ -151,6 +152,36 @@ export declare class ChordAnalytics<T extends ObjectTypes = ObjectTypes> {
151
152
  */
152
153
  private sendPageEvent;
153
154
  reset: () => Promise<void>;
155
+ /**
156
+ * Handler for postMessage events from the Shopify Web Pixel sandbox.
157
+ * Bound to the instance for proper `this` context and cleanup.
158
+ */
159
+ private webPixelMessageHandler;
160
+ /**
161
+ * Set up a listener for postMessage events from the Shopify Web Pixel sandbox.
162
+ * This allows the lightweight web pixel (running in Shopify's sandboxed iframe)
163
+ * to forward analytics events to this ChordAnalytics instance on the storefront.
164
+ *
165
+ * The lightweight web pixel sends messages in the format:
166
+ * - { source: 'chord-web-pixel', type: 'track', payload: [eventName, properties] }
167
+ * - { source: 'chord-web-pixel', type: 'identify', payload: [userId, traits] }
168
+ * - { source: 'chord-web-pixel', type: 'page', payload: [properties] }
169
+ *
170
+ * @returns A cleanup function to remove the listener
171
+ */
172
+ setupWebPixelListener: () => (() => void);
173
+ /**
174
+ * Sync CDP anonymous ID and Google Client ID to Shopify cart attributes.
175
+ * This enables server-side events (webhooks) to correlate with client-side tracking.
176
+ *
177
+ * By default, syncs:
178
+ * - `_cdpAnonymousId`: from the `__eventn_id` cookie (Chord/Jitsu CDP)
179
+ * - `_googleClientId`: from the `_ga` cookie (Google Analytics)
180
+ *
181
+ * @param options - Optional configuration for the sync
182
+ * @returns The updated cart, or undefined if no update was needed or an error occurred
183
+ */
184
+ syncIdentifiersToCart: (options?: IdentifierSyncOptions) => Promise<ShopifyCart | undefined>;
154
185
  trackCartViewed: (props: CartViewedInput<T>, options?: AnyOptions) => Promise<void>;
155
186
  trackCheckoutStarted: (props: CheckoutStartedInput<T>, options?: AnyOptions) => Promise<void>;
156
187
  trackCheckoutStepCompleted: (props: CheckoutStepCompletedInput, options?: AnyOptions) => Promise<void>;
@@ -61,6 +61,7 @@ var cdp_js_1 = require("./cdp.js");
61
61
  var index_js_1 = require("./consent/index.js");
62
62
  var index_js_2 = require("../validators/index.js");
63
63
  var utils_js_1 = require("../utils.js");
64
+ var identifiers_js_1 = require("./identifiers.js");
64
65
  var DEBUG_MODE_STORAGE_KEY = 'chordcommerce/analytics/debug';
65
66
  var CONSENT_QUEUE_STORAGE_KEY = 'chordcommerce/analytics/consent-queue';
66
67
  var CONSENT_QUEUE_TTL_MS = 5 * 60 * 1000; // 5 minutes
@@ -69,7 +70,7 @@ var CONSENT_QUEUE_MAX_EVENTS = 50; // Max number of events to queue
69
70
  var ChordAnalytics = /** @class */ (function () {
70
71
  function ChordAnalytics(options) {
71
72
  var _this = this;
72
- var _a, _b, _c;
73
+ var _a, _b, _c, _d, _e;
73
74
  /**
74
75
  * Allows snippet.js to detect whether this library has been initialized yet.
75
76
  */
@@ -791,6 +792,107 @@ var ChordAnalytics = /** @class */ (function () {
791
792
  }
792
793
  });
793
794
  }); };
795
+ /**
796
+ * Handler for postMessage events from the Shopify Web Pixel sandbox.
797
+ * Bound to the instance for proper `this` context and cleanup.
798
+ */
799
+ this.webPixelMessageHandler = null;
800
+ /**
801
+ * Set up a listener for postMessage events from the Shopify Web Pixel sandbox.
802
+ * This allows the lightweight web pixel (running in Shopify's sandboxed iframe)
803
+ * to forward analytics events to this ChordAnalytics instance on the storefront.
804
+ *
805
+ * The lightweight web pixel sends messages in the format:
806
+ * - { source: 'chord-web-pixel', type: 'track', payload: [eventName, properties] }
807
+ * - { source: 'chord-web-pixel', type: 'identify', payload: [userId, traits] }
808
+ * - { source: 'chord-web-pixel', type: 'page', payload: [properties] }
809
+ *
810
+ * @returns A cleanup function to remove the listener
811
+ */
812
+ this.setupWebPixelListener = function () {
813
+ if (typeof window === 'undefined') {
814
+ _this.logger('[Chord Analytics]: setupWebPixelListener called in non-browser environment');
815
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
816
+ return function () { };
817
+ }
818
+ // Remove existing listener if one exists
819
+ if (_this.webPixelMessageHandler) {
820
+ window.removeEventListener('message', _this.webPixelMessageHandler);
821
+ }
822
+ _this.webPixelMessageHandler = function (event) {
823
+ // Validate message structure
824
+ if (!event.data || typeof event.data !== 'object')
825
+ return;
826
+ if (event.data.source !== 'chord-web-pixel')
827
+ return;
828
+ var message = event.data;
829
+ if (_this.options.enableLogging && _this.options.debug) {
830
+ // eslint-disable-next-line no-console
831
+ console.log('[Chord Analytics DEBUG]: Received web pixel message:', message.type, message.payload);
832
+ }
833
+ try {
834
+ switch (message.type) {
835
+ case 'track': {
836
+ var _a = message.payload, eventName = _a[0], properties = _a[1];
837
+ if (eventName) {
838
+ _this.track(eventName, properties);
839
+ }
840
+ break;
841
+ }
842
+ case 'identify': {
843
+ var _b = message.payload, userId = _b[0], traits = _b[1];
844
+ _this.identify(userId, traits);
845
+ break;
846
+ }
847
+ case 'page': {
848
+ var properties = message.payload[0];
849
+ _this.page(properties);
850
+ break;
851
+ }
852
+ default:
853
+ _this.logger("[Chord Analytics]: Unknown web pixel message type: ".concat(message.type));
854
+ }
855
+ }
856
+ catch (error) {
857
+ _this.logger('[Chord Analytics]: Error processing web pixel message:', error);
858
+ }
859
+ };
860
+ window.addEventListener('message', _this.webPixelMessageHandler);
861
+ _this.logger('[Chord Analytics]: Web pixel listener initialized');
862
+ // Return cleanup function
863
+ return function () {
864
+ if (_this.webPixelMessageHandler) {
865
+ window.removeEventListener('message', _this.webPixelMessageHandler);
866
+ _this.webPixelMessageHandler = null;
867
+ _this.logger('[Chord Analytics]: Web pixel listener removed');
868
+ }
869
+ };
870
+ };
871
+ /**
872
+ * Sync CDP anonymous ID and Google Client ID to Shopify cart attributes.
873
+ * This enables server-side events (webhooks) to correlate with client-side tracking.
874
+ *
875
+ * By default, syncs:
876
+ * - `_cdpAnonymousId`: from the `__eventn_id` cookie (Chord/Jitsu CDP)
877
+ * - `_googleClientId`: from the `_ga` cookie (Google Analytics)
878
+ *
879
+ * @param options - Optional configuration for the sync
880
+ * @returns The updated cart, or undefined if no update was needed or an error occurred
881
+ */
882
+ this.syncIdentifiersToCart = function (options) { return __awaiter(_this, void 0, void 0, function () {
883
+ var result;
884
+ return __generator(this, function (_a) {
885
+ switch (_a.label) {
886
+ case 0: return [4 /*yield*/, (0, identifiers_js_1.syncIdentifiersToCart)(options)];
887
+ case 1:
888
+ result = _a.sent();
889
+ if (result) {
890
+ this.logger('[Chord Analytics]: Identifiers synced to cart');
891
+ }
892
+ return [2 /*return*/, result];
893
+ }
894
+ });
895
+ }); };
794
896
  // TODO: Add support for props.products
795
897
  this.trackCartViewed = function (props, options) { return __awaiter(_this, void 0, void 0, function () {
796
898
  var cart, payload, formattedPayload;
@@ -1345,6 +1447,14 @@ var ChordAnalytics = /** @class */ (function () {
1345
1447
  window[this.options.namespace] = (window === null || window === void 0 ? void 0 : window[this.options.namespace]) || this;
1346
1448
  this.initializeCdp();
1347
1449
  }
1450
+ // Automatically set up web pixel listener if enabled
1451
+ if ((_d = this.options) === null || _d === void 0 ? void 0 : _d.enableWebPixelListener) {
1452
+ this.setupWebPixelListener();
1453
+ }
1454
+ // Automatically sync identifiers to cart if enabled
1455
+ if ((_e = this.options) === null || _e === void 0 ? void 0 : _e.enableIdentifierSync) {
1456
+ this.syncIdentifiersToCart();
1457
+ }
1348
1458
  }
1349
1459
  /**
1350
1460
  * Initialize CDP snippet and wait for it to be ready.
@@ -1,2 +1,3 @@
1
1
  export * from './client/index.js';
2
+ export * from './client/identifiers.js';
2
3
  export * from './types/index.js';
package/dist/cjs/index.js CHANGED
@@ -15,4 +15,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./client/index.js"), exports);
18
+ __exportStar(require("./client/identifiers.js"), exports);
18
19
  __exportStar(require("./types/index.js"), exports);
@@ -34,6 +34,18 @@ export interface ChordAnalyticsOptions<T extends ObjectTypes = ObjectTypes> {
34
34
  awaitConsent?: boolean;
35
35
  debug?: boolean;
36
36
  enableLogging?: boolean;
37
+ /**
38
+ * When true, automatically sets up a postMessage listener for events from
39
+ * the Shopify Custom Web Pixel sandbox. This enables the lightweight web pixel
40
+ * to forward analytics events to this ChordAnalytics instance.
41
+ */
42
+ enableWebPixelListener?: boolean;
43
+ /**
44
+ * When true, automatically syncs CDP anonymous ID and Google Client ID
45
+ * to Shopify cart attributes. This enables server-side events (webhooks)
46
+ * to correlate with client-side tracking.
47
+ */
48
+ enableIdentifierSync?: boolean;
37
49
  formatters: ChordAnalyticsFormatters<T>;
38
50
  namespace?: string;
39
51
  stripNull?: boolean;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Shopify Identifier Sync
3
+ *
4
+ * Syncs CDP anonymous ID and Google Client ID to Shopify cart attributes.
5
+ * This enables server-side events (webhooks) to correlate with client-side tracking.
6
+ */
7
+ export interface ShopifyCart {
8
+ attributes?: Record<string, unknown>;
9
+ [key: string]: unknown;
10
+ }
11
+ export interface IdentifierSyncOptions {
12
+ /**
13
+ * Base URL for Shopify AJAX API calls. Defaults to window.location.origin
14
+ */
15
+ baseUrl?: string;
16
+ /**
17
+ * Locale path for Shopify routes. Defaults to window.Shopify.routes.root or '/'
18
+ */
19
+ localePath?: string;
20
+ /**
21
+ * Custom identifier getters. By default, reads CDP anonymous ID and Google Client ID.
22
+ */
23
+ identifiers?: () => Record<string, string>;
24
+ }
25
+ /**
26
+ * Get a cookie value by name
27
+ */
28
+ export declare const getCookie: (name: string) => string | undefined;
29
+ /**
30
+ * Get default identifiers (CDP anonymous ID and Google Client ID)
31
+ */
32
+ export declare const getDefaultIdentifiers: () => Record<string, string>;
33
+ /**
34
+ * Fetch the current Shopify cart
35
+ */
36
+ export declare const getShopifyCart: (baseUrl?: string, localePath?: string) => Promise<ShopifyCart>;
37
+ /**
38
+ * Update Shopify cart attributes
39
+ */
40
+ export declare const updateShopifyCartAttributes: (attributes: Record<string, string>, baseUrl?: string, localePath?: string) => Promise<ShopifyCart>;
41
+ /**
42
+ * Sync identifiers to Shopify cart attributes.
43
+ *
44
+ * This function:
45
+ * 1. Gets the current cart
46
+ * 2. Checks if identifiers already exist
47
+ * 3. If not, updates the cart with the identifiers
48
+ *
49
+ * @returns The updated cart, or undefined if no update was needed
50
+ */
51
+ export declare const syncIdentifiersToCart: (options?: IdentifierSyncOptions) => Promise<ShopifyCart | undefined>;