brainerce 1.56.0 → 1.57.0

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
@@ -2457,6 +2457,10 @@ const checkout = await client.setCheckoutCustomer(checkoutId, {
2457
2457
  notes: 'Please leave the package at the door', // Optional order note (max 2000 chars)
2458
2458
  // analyticsClientId / analyticsSessionId: auto-attached if you called
2459
2459
  // loadGoogleAnalytics() — no need to pass these yourself.
2460
+ // trafficReferrerHost / trafficUtm*: auto-attached from the SDK's own
2461
+ // traffic-attribution capture (external referrer + utm, 30-day window) so
2462
+ // the dashboard can report "orders from ChatGPT / Google / …". Nothing to
2463
+ // wire up; pass explicitly only to override.
2460
2464
  });
2461
2465
  ```
2462
2466
 
@@ -4094,6 +4098,15 @@ const cart = await client.createCart();
4094
4098
  await client.addToCart(cart.id, { productId: 'prod_abc', quantity: 1 });
4095
4099
  ```
4096
4100
 
4101
+ ### Traffic attribution (automatic, zero-config)
4102
+
4103
+ The SDK also records where each visit came from — the external referrer host
4104
+ and any `utm_source`/`utm_medium`/`utm_campaign` — as a **last non-direct
4105
+ touch** (`brainerce_attr` in localStorage, 30-day window). The captured values
4106
+ are auto-attached to `setCheckoutCustomer()` / `setShippingAddress()` and end
4107
+ up on the order, powering the dashboard's "orders from ChatGPT / Google / …"
4108
+ reporting. Nothing to configure; a value you pass explicitly always wins.
4109
+
4097
4110
  What this does:
4098
4111
 
4099
4112
  - Idempotently injects `gtag.js` and initializes `dataLayer` (skips injection if you're already loading `gtag.js` yourself — safe to call either way).
package/dist/index.d.mts CHANGED
@@ -3294,6 +3294,16 @@ interface SetCheckoutCustomerDto {
3294
3294
  */
3295
3295
  analyticsClientId?: string;
3296
3296
  analyticsSessionId?: string;
3297
+ /**
3298
+ * Traffic attribution (last non-direct touch) — auto-attached by the SDK
3299
+ * from its `brainerce_attr` capture (external referrer host + utm params,
3300
+ * 30-day window). Lets the dashboard report "orders from ChatGPT/Google/…".
3301
+ * Pass explicitly to override; omit to use the captured values.
3302
+ */
3303
+ trafficReferrerHost?: string;
3304
+ trafficUtmSource?: string;
3305
+ trafficUtmMedium?: string;
3306
+ trafficUtmCampaign?: string;
3297
3307
  }
3298
3308
  /**
3299
3309
  * Shipping address with customer email (required for checkout).
@@ -3327,6 +3337,16 @@ interface SetShippingAddressDto {
3327
3337
  */
3328
3338
  analyticsClientId?: string;
3329
3339
  analyticsSessionId?: string;
3340
+ /**
3341
+ * Traffic attribution (last non-direct touch) — auto-attached by the SDK
3342
+ * from its `brainerce_attr` capture (external referrer host + utm params,
3343
+ * 30-day window). Lets the dashboard report "orders from ChatGPT/Google/…".
3344
+ * Pass explicitly to override; omit to use the captured values.
3345
+ */
3346
+ trafficReferrerHost?: string;
3347
+ trafficUtmSource?: string;
3348
+ trafficUtmMedium?: string;
3349
+ trafficUtmCampaign?: string;
3330
3350
  /**
3331
3351
  * The `placeId` of the autocomplete suggestion the shopper picked (from
3332
3352
  * `addressAutocomplete()`). Send it whenever the address came from the
@@ -6847,6 +6867,26 @@ declare class BrainerceClient {
6847
6867
  * No-op (returns `dto` unchanged) if `loadGoogleAnalytics()` was never
6848
6868
  * called, or if it hasn't resolved any ids by the time this is awaited.
6849
6869
  */
6870
+ /** localStorage key holding the last non-direct-touch attribution blob. */
6871
+ private readonly TRAFFIC_ATTR_KEY;
6872
+ /** Attribution older than this is stale and never forwarded (classic 30-day window). */
6873
+ private static readonly TRAFFIC_ATTR_MAX_AGE_MS;
6874
+ /**
6875
+ * Record the visit's traffic origin — LAST NON-DIRECT TOUCH semantics: an
6876
+ * external referrer or any utm_source overwrites the stored blob; a direct
6877
+ * or internal navigation keeps the previous touch. Runs once per client
6878
+ * construction (i.e. per page load in browser storefronts) and must never
6879
+ * throw — attribution is telemetry, the storefront always wins.
6880
+ */
6881
+ private captureTrafficAttribution;
6882
+ /** The stored attribution as request-body fields, or null when absent/stale. */
6883
+ private getTrafficAttribution;
6884
+ /**
6885
+ * Merge the stored traffic attribution onto a request body — only for
6886
+ * fields the caller didn't set explicitly (explicit values always win),
6887
+ * mirroring `withAnalyticsStitchIds`. No-op outside the browser.
6888
+ */
6889
+ private withTrafficAttribution;
6850
6890
  private withAnalyticsStitchIds;
6851
6891
  /**
6852
6892
  * Drop the fields `getAddressDetails()` returns that no address endpoint
package/dist/index.d.ts CHANGED
@@ -3294,6 +3294,16 @@ interface SetCheckoutCustomerDto {
3294
3294
  */
3295
3295
  analyticsClientId?: string;
3296
3296
  analyticsSessionId?: string;
3297
+ /**
3298
+ * Traffic attribution (last non-direct touch) — auto-attached by the SDK
3299
+ * from its `brainerce_attr` capture (external referrer host + utm params,
3300
+ * 30-day window). Lets the dashboard report "orders from ChatGPT/Google/…".
3301
+ * Pass explicitly to override; omit to use the captured values.
3302
+ */
3303
+ trafficReferrerHost?: string;
3304
+ trafficUtmSource?: string;
3305
+ trafficUtmMedium?: string;
3306
+ trafficUtmCampaign?: string;
3297
3307
  }
3298
3308
  /**
3299
3309
  * Shipping address with customer email (required for checkout).
@@ -3327,6 +3337,16 @@ interface SetShippingAddressDto {
3327
3337
  */
3328
3338
  analyticsClientId?: string;
3329
3339
  analyticsSessionId?: string;
3340
+ /**
3341
+ * Traffic attribution (last non-direct touch) — auto-attached by the SDK
3342
+ * from its `brainerce_attr` capture (external referrer host + utm params,
3343
+ * 30-day window). Lets the dashboard report "orders from ChatGPT/Google/…".
3344
+ * Pass explicitly to override; omit to use the captured values.
3345
+ */
3346
+ trafficReferrerHost?: string;
3347
+ trafficUtmSource?: string;
3348
+ trafficUtmMedium?: string;
3349
+ trafficUtmCampaign?: string;
3330
3350
  /**
3331
3351
  * The `placeId` of the autocomplete suggestion the shopper picked (from
3332
3352
  * `addressAutocomplete()`). Send it whenever the address came from the
@@ -6847,6 +6867,26 @@ declare class BrainerceClient {
6847
6867
  * No-op (returns `dto` unchanged) if `loadGoogleAnalytics()` was never
6848
6868
  * called, or if it hasn't resolved any ids by the time this is awaited.
6849
6869
  */
6870
+ /** localStorage key holding the last non-direct-touch attribution blob. */
6871
+ private readonly TRAFFIC_ATTR_KEY;
6872
+ /** Attribution older than this is stale and never forwarded (classic 30-day window). */
6873
+ private static readonly TRAFFIC_ATTR_MAX_AGE_MS;
6874
+ /**
6875
+ * Record the visit's traffic origin — LAST NON-DIRECT TOUCH semantics: an
6876
+ * external referrer or any utm_source overwrites the stored blob; a direct
6877
+ * or internal navigation keeps the previous touch. Runs once per client
6878
+ * construction (i.e. per page load in browser storefronts) and must never
6879
+ * throw — attribution is telemetry, the storefront always wins.
6880
+ */
6881
+ private captureTrafficAttribution;
6882
+ /** The stored attribution as request-body fields, or null when absent/stale. */
6883
+ private getTrafficAttribution;
6884
+ /**
6885
+ * Merge the stored traffic attribution onto a request body — only for
6886
+ * fields the caller didn't set explicitly (explicit values always win),
6887
+ * mirroring `withAnalyticsStitchIds`. No-op outside the browser.
6888
+ */
6889
+ private withTrafficAttribution;
6850
6890
  private withAnalyticsStitchIds;
6851
6891
  /**
6852
6892
  * Drop the fields `getAddressDetails()` returns that no address endpoint
package/dist/index.js CHANGED
@@ -299,6 +299,14 @@ var _BrainerceClient = class _BrainerceClient {
299
299
  * This is needed because Stripe redirects lose in-memory state.
300
300
  */
301
301
  this.ACTIVE_CHECKOUT_KEY = "brainerce_active_checkout";
302
+ /**
303
+ * Merge the resolved GA4 stitch ids onto a request body — only for fields
304
+ * the caller didn't already set explicitly (explicit values always win).
305
+ * No-op (returns `dto` unchanged) if `loadGoogleAnalytics()` was never
306
+ * called, or if it hasn't resolved any ids by the time this is awaited.
307
+ */
308
+ /** localStorage key holding the last non-direct-touch attribution blob. */
309
+ this.TRAFFIC_ATTR_KEY = "brainerce_attr";
302
310
  // -------------------- Contact Forms (schema) --------------------
303
311
  /**
304
312
  * List active contact forms configured for the store.
@@ -704,6 +712,7 @@ var _BrainerceClient = class _BrainerceClient {
704
712
  this.onCartReset = options.onCartReset;
705
713
  this.hydrateSessionCart();
706
714
  this.detectRecoverCartFromUrl();
715
+ this.captureTrafficAttribution();
707
716
  }
708
717
  // -------------------- Locale --------------------
709
718
  /**
@@ -1526,11 +1535,82 @@ var _BrainerceClient = class _BrainerceClient {
1526
1535
  }
1527
1536
  }
1528
1537
  /**
1529
- * Merge the resolved GA4 stitch ids onto a request body — only for fields
1530
- * the caller didn't already set explicitly (explicit values always win).
1531
- * No-op (returns `dto` unchanged) if `loadGoogleAnalytics()` was never
1532
- * called, or if it hasn't resolved any ids by the time this is awaited.
1538
+ * Record the visit's traffic origin LAST NON-DIRECT TOUCH semantics: an
1539
+ * external referrer or any utm_source overwrites the stored blob; a direct
1540
+ * or internal navigation keeps the previous touch. Runs once per client
1541
+ * construction (i.e. per page load in browser storefronts) and must never
1542
+ * throw — attribution is telemetry, the storefront always wins.
1543
+ */
1544
+ captureTrafficAttribution() {
1545
+ try {
1546
+ if (typeof window === "undefined" || !window.localStorage) return;
1547
+ const params = new URLSearchParams(window.location.search);
1548
+ const utmSource = params.get("utm_source") || void 0;
1549
+ const utmMedium = params.get("utm_medium") || void 0;
1550
+ const utmCampaign = params.get("utm_campaign") || void 0;
1551
+ let referrerHost;
1552
+ if (document.referrer) {
1553
+ try {
1554
+ const ref = new URL(document.referrer);
1555
+ if (ref.hostname && ref.hostname !== window.location.hostname) {
1556
+ referrerHost = ref.hostname.toLowerCase().replace(/^www\./, "");
1557
+ }
1558
+ } catch {
1559
+ }
1560
+ }
1561
+ if (!referrerHost && !utmSource) return;
1562
+ window.localStorage.setItem(
1563
+ this.TRAFFIC_ATTR_KEY,
1564
+ JSON.stringify({ referrerHost, utmSource, utmMedium, utmCampaign, at: Date.now() })
1565
+ );
1566
+ } catch {
1567
+ }
1568
+ }
1569
+ /** The stored attribution as request-body fields, or null when absent/stale. */
1570
+ getTrafficAttribution() {
1571
+ try {
1572
+ if (typeof window === "undefined" || !window.localStorage) return null;
1573
+ const raw = window.localStorage.getItem(this.TRAFFIC_ATTR_KEY);
1574
+ if (!raw) return null;
1575
+ const blob = JSON.parse(raw);
1576
+ if (!blob || typeof blob !== "object") return null;
1577
+ if (typeof blob.at !== "number" || Date.now() - blob.at > _BrainerceClient.TRAFFIC_ATTR_MAX_AGE_MS) {
1578
+ return null;
1579
+ }
1580
+ const out = {};
1581
+ if (typeof blob.referrerHost === "string" && blob.referrerHost) {
1582
+ out.trafficReferrerHost = blob.referrerHost.slice(0, 253);
1583
+ }
1584
+ if (typeof blob.utmSource === "string" && blob.utmSource) {
1585
+ out.trafficUtmSource = blob.utmSource.slice(0, 150);
1586
+ }
1587
+ if (typeof blob.utmMedium === "string" && blob.utmMedium) {
1588
+ out.trafficUtmMedium = blob.utmMedium.slice(0, 150);
1589
+ }
1590
+ if (typeof blob.utmCampaign === "string" && blob.utmCampaign) {
1591
+ out.trafficUtmCampaign = blob.utmCampaign.slice(0, 150);
1592
+ }
1593
+ return Object.keys(out).length > 0 ? out : null;
1594
+ } catch {
1595
+ return null;
1596
+ }
1597
+ }
1598
+ /**
1599
+ * Merge the stored traffic attribution onto a request body — only for
1600
+ * fields the caller didn't set explicitly (explicit values always win),
1601
+ * mirroring `withAnalyticsStitchIds`. No-op outside the browser.
1533
1602
  */
1603
+ withTrafficAttribution(dto) {
1604
+ const attr = this.getTrafficAttribution();
1605
+ if (!attr) return dto;
1606
+ return {
1607
+ ...dto ?? {},
1608
+ trafficReferrerHost: dto?.trafficReferrerHost ?? attr.trafficReferrerHost,
1609
+ trafficUtmSource: dto?.trafficUtmSource ?? attr.trafficUtmSource,
1610
+ trafficUtmMedium: dto?.trafficUtmMedium ?? attr.trafficUtmMedium,
1611
+ trafficUtmCampaign: dto?.trafficUtmCampaign ?? attr.trafficUtmCampaign
1612
+ };
1613
+ }
1534
1614
  async withAnalyticsStitchIds(dto) {
1535
1615
  if (!this._ga4StitchPromise) return dto;
1536
1616
  try {
@@ -5387,7 +5467,7 @@ var _BrainerceClient = class _BrainerceClient {
5387
5467
  * ```
5388
5468
  */
5389
5469
  async setCheckoutCustomer(checkoutId, data) {
5390
- const body = await this.withAnalyticsStitchIds(data);
5470
+ const body = this.withTrafficAttribution(await this.withAnalyticsStitchIds(data));
5391
5471
  if (this.isVibeCodedMode()) {
5392
5472
  return this.vibeCodedRequest(
5393
5473
  "PATCH",
@@ -5489,7 +5569,9 @@ var _BrainerceClient = class _BrainerceClient {
5489
5569
  * ```
5490
5570
  */
5491
5571
  async setShippingAddress(checkoutId, address) {
5492
- const body = await this.withAnalyticsStitchIds(this.stripResolvedOnlyAddressFields(address));
5572
+ const body = this.withTrafficAttribution(
5573
+ await this.withAnalyticsStitchIds(this.stripResolvedOnlyAddressFields(address))
5574
+ );
5493
5575
  if (this.isVibeCodedMode()) {
5494
5576
  return this.vibeCodedRequest(
5495
5577
  "PATCH",
@@ -9716,6 +9798,8 @@ _BrainerceClient.RESOLVED_ONLY_ADDRESS_FIELDS = [
9716
9798
  "lng",
9717
9799
  "formattedAddress"
9718
9800
  ];
9801
+ /** Attribution older than this is stale and never forwarded (classic 30-day window). */
9802
+ _BrainerceClient.TRAFFIC_ATTR_MAX_AGE_MS = 30 * 24 * 3600 * 1e3;
9719
9803
  var BrainerceClient = _BrainerceClient;
9720
9804
  var BrainerceError = class extends Error {
9721
9805
  constructor(message, statusCode, details) {
package/dist/index.mjs CHANGED
@@ -212,6 +212,14 @@ var _BrainerceClient = class _BrainerceClient {
212
212
  * This is needed because Stripe redirects lose in-memory state.
213
213
  */
214
214
  this.ACTIVE_CHECKOUT_KEY = "brainerce_active_checkout";
215
+ /**
216
+ * Merge the resolved GA4 stitch ids onto a request body — only for fields
217
+ * the caller didn't already set explicitly (explicit values always win).
218
+ * No-op (returns `dto` unchanged) if `loadGoogleAnalytics()` was never
219
+ * called, or if it hasn't resolved any ids by the time this is awaited.
220
+ */
221
+ /** localStorage key holding the last non-direct-touch attribution blob. */
222
+ this.TRAFFIC_ATTR_KEY = "brainerce_attr";
215
223
  // -------------------- Contact Forms (schema) --------------------
216
224
  /**
217
225
  * List active contact forms configured for the store.
@@ -617,6 +625,7 @@ var _BrainerceClient = class _BrainerceClient {
617
625
  this.onCartReset = options.onCartReset;
618
626
  this.hydrateSessionCart();
619
627
  this.detectRecoverCartFromUrl();
628
+ this.captureTrafficAttribution();
620
629
  }
621
630
  // -------------------- Locale --------------------
622
631
  /**
@@ -1439,11 +1448,82 @@ var _BrainerceClient = class _BrainerceClient {
1439
1448
  }
1440
1449
  }
1441
1450
  /**
1442
- * Merge the resolved GA4 stitch ids onto a request body — only for fields
1443
- * the caller didn't already set explicitly (explicit values always win).
1444
- * No-op (returns `dto` unchanged) if `loadGoogleAnalytics()` was never
1445
- * called, or if it hasn't resolved any ids by the time this is awaited.
1451
+ * Record the visit's traffic origin LAST NON-DIRECT TOUCH semantics: an
1452
+ * external referrer or any utm_source overwrites the stored blob; a direct
1453
+ * or internal navigation keeps the previous touch. Runs once per client
1454
+ * construction (i.e. per page load in browser storefronts) and must never
1455
+ * throw — attribution is telemetry, the storefront always wins.
1456
+ */
1457
+ captureTrafficAttribution() {
1458
+ try {
1459
+ if (typeof window === "undefined" || !window.localStorage) return;
1460
+ const params = new URLSearchParams(window.location.search);
1461
+ const utmSource = params.get("utm_source") || void 0;
1462
+ const utmMedium = params.get("utm_medium") || void 0;
1463
+ const utmCampaign = params.get("utm_campaign") || void 0;
1464
+ let referrerHost;
1465
+ if (document.referrer) {
1466
+ try {
1467
+ const ref = new URL(document.referrer);
1468
+ if (ref.hostname && ref.hostname !== window.location.hostname) {
1469
+ referrerHost = ref.hostname.toLowerCase().replace(/^www\./, "");
1470
+ }
1471
+ } catch {
1472
+ }
1473
+ }
1474
+ if (!referrerHost && !utmSource) return;
1475
+ window.localStorage.setItem(
1476
+ this.TRAFFIC_ATTR_KEY,
1477
+ JSON.stringify({ referrerHost, utmSource, utmMedium, utmCampaign, at: Date.now() })
1478
+ );
1479
+ } catch {
1480
+ }
1481
+ }
1482
+ /** The stored attribution as request-body fields, or null when absent/stale. */
1483
+ getTrafficAttribution() {
1484
+ try {
1485
+ if (typeof window === "undefined" || !window.localStorage) return null;
1486
+ const raw = window.localStorage.getItem(this.TRAFFIC_ATTR_KEY);
1487
+ if (!raw) return null;
1488
+ const blob = JSON.parse(raw);
1489
+ if (!blob || typeof blob !== "object") return null;
1490
+ if (typeof blob.at !== "number" || Date.now() - blob.at > _BrainerceClient.TRAFFIC_ATTR_MAX_AGE_MS) {
1491
+ return null;
1492
+ }
1493
+ const out = {};
1494
+ if (typeof blob.referrerHost === "string" && blob.referrerHost) {
1495
+ out.trafficReferrerHost = blob.referrerHost.slice(0, 253);
1496
+ }
1497
+ if (typeof blob.utmSource === "string" && blob.utmSource) {
1498
+ out.trafficUtmSource = blob.utmSource.slice(0, 150);
1499
+ }
1500
+ if (typeof blob.utmMedium === "string" && blob.utmMedium) {
1501
+ out.trafficUtmMedium = blob.utmMedium.slice(0, 150);
1502
+ }
1503
+ if (typeof blob.utmCampaign === "string" && blob.utmCampaign) {
1504
+ out.trafficUtmCampaign = blob.utmCampaign.slice(0, 150);
1505
+ }
1506
+ return Object.keys(out).length > 0 ? out : null;
1507
+ } catch {
1508
+ return null;
1509
+ }
1510
+ }
1511
+ /**
1512
+ * Merge the stored traffic attribution onto a request body — only for
1513
+ * fields the caller didn't set explicitly (explicit values always win),
1514
+ * mirroring `withAnalyticsStitchIds`. No-op outside the browser.
1446
1515
  */
1516
+ withTrafficAttribution(dto) {
1517
+ const attr = this.getTrafficAttribution();
1518
+ if (!attr) return dto;
1519
+ return {
1520
+ ...dto ?? {},
1521
+ trafficReferrerHost: dto?.trafficReferrerHost ?? attr.trafficReferrerHost,
1522
+ trafficUtmSource: dto?.trafficUtmSource ?? attr.trafficUtmSource,
1523
+ trafficUtmMedium: dto?.trafficUtmMedium ?? attr.trafficUtmMedium,
1524
+ trafficUtmCampaign: dto?.trafficUtmCampaign ?? attr.trafficUtmCampaign
1525
+ };
1526
+ }
1447
1527
  async withAnalyticsStitchIds(dto) {
1448
1528
  if (!this._ga4StitchPromise) return dto;
1449
1529
  try {
@@ -5300,7 +5380,7 @@ var _BrainerceClient = class _BrainerceClient {
5300
5380
  * ```
5301
5381
  */
5302
5382
  async setCheckoutCustomer(checkoutId, data) {
5303
- const body = await this.withAnalyticsStitchIds(data);
5383
+ const body = this.withTrafficAttribution(await this.withAnalyticsStitchIds(data));
5304
5384
  if (this.isVibeCodedMode()) {
5305
5385
  return this.vibeCodedRequest(
5306
5386
  "PATCH",
@@ -5402,7 +5482,9 @@ var _BrainerceClient = class _BrainerceClient {
5402
5482
  * ```
5403
5483
  */
5404
5484
  async setShippingAddress(checkoutId, address) {
5405
- const body = await this.withAnalyticsStitchIds(this.stripResolvedOnlyAddressFields(address));
5485
+ const body = this.withTrafficAttribution(
5486
+ await this.withAnalyticsStitchIds(this.stripResolvedOnlyAddressFields(address))
5487
+ );
5406
5488
  if (this.isVibeCodedMode()) {
5407
5489
  return this.vibeCodedRequest(
5408
5490
  "PATCH",
@@ -9629,6 +9711,8 @@ _BrainerceClient.RESOLVED_ONLY_ADDRESS_FIELDS = [
9629
9711
  "lng",
9630
9712
  "formattedAddress"
9631
9713
  ];
9714
+ /** Attribution older than this is stale and never forwarded (classic 30-day window). */
9715
+ _BrainerceClient.TRAFFIC_ATTR_MAX_AGE_MS = 30 * 24 * 3600 * 1e3;
9632
9716
  var BrainerceClient = _BrainerceClient;
9633
9717
  var BrainerceError = class extends Error {
9634
9718
  constructor(message, statusCode, details) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainerce",
3
- "version": "1.56.0",
3
+ "version": "1.57.0",
4
4
  "description": "Official SDK for building e-commerce storefronts with Brainerce Platform. Perfect for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",