@funnelfox/billing 0.7.1 → 0.8.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.
@@ -374,6 +374,9 @@
374
374
  return v.toString(16);
375
375
  });
376
376
  }
377
+ function isBrowser() {
378
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
379
+ }
377
380
  function sleep(ms) {
378
381
  return new Promise(resolve => setTimeout(resolve, ms));
379
382
  }
@@ -492,7 +495,7 @@
492
495
  /**
493
496
  * @fileoverview Constants for Funnefox SDK
494
497
  */
495
- const SDK_VERSION = '0.7.1';
498
+ const SDK_VERSION = '0.8.0-beta.1';
496
499
  const DEFAULTS = {
497
500
  BASE_URL: 'https://billing.funnelfox.com',
498
501
  REGION: 'default',
@@ -1499,6 +1502,219 @@
1499
1502
  });
1500
1503
  }
1501
1504
 
1505
+ const MAX_QUERY_LENGTH = 1800;
1506
+ function getErrorImage(orgId, options) {
1507
+ if (typeof document === 'undefined') {
1508
+ return;
1509
+ }
1510
+ const params = new URLSearchParams({
1511
+ message: truncate(options.message, 500),
1512
+ code: options.code || 'SDK_ERROR',
1513
+ timestamp: Date.now().toString(),
1514
+ sdk_version: SDK_VERSION,
1515
+ });
1516
+ if (options.req_id) {
1517
+ appendIfFits(params, 'req_id', options.req_id, MAX_QUERY_LENGTH);
1518
+ }
1519
+ if (options.context) {
1520
+ Object.entries(options.context).forEach(([key, value]) => {
1521
+ if (value !== undefined && value !== null) {
1522
+ appendIfFits(params, key, truncate(String(value), 1000), MAX_QUERY_LENGTH);
1523
+ }
1524
+ });
1525
+ }
1526
+ const url = `https://billing.funnelfox.com/sdk_report/${encodeURIComponent(orgId)}/crash?${params.toString()}`;
1527
+ const img = new Image();
1528
+ img.src = url;
1529
+ img.style.display = 'none';
1530
+ img.onload = () => {
1531
+ img.remove();
1532
+ };
1533
+ img.onerror = () => {
1534
+ img.remove();
1535
+ };
1536
+ (document.body || document.documentElement)?.appendChild(img);
1537
+ }
1538
+ function truncate(value, maxLength) {
1539
+ return value.length > maxLength ? value.slice(0, maxLength) : value;
1540
+ }
1541
+ function appendIfFits(params, key, value, maxLength) {
1542
+ const nextParams = new URLSearchParams(params);
1543
+ nextParams.append(key, value);
1544
+ if (nextParams.toString().length > maxLength) {
1545
+ return false;
1546
+ }
1547
+ params.append(key, value);
1548
+ return true;
1549
+ }
1550
+
1551
+ let activeScope = null;
1552
+ const recentSignatures = new Map();
1553
+ const DEDUPE_WINDOW_MS = 3000;
1554
+ let isListening = false;
1555
+ function startUnhandledErrorTelemetry(scope) {
1556
+ if (!scope.enabled || !isBrowser()) {
1557
+ return () => { };
1558
+ }
1559
+ activeScope = scope;
1560
+ attachListeners();
1561
+ return () => {
1562
+ stopUnhandledErrorTelemetry(scope.id);
1563
+ };
1564
+ }
1565
+ function stopUnhandledErrorTelemetry(scopeId) {
1566
+ if (activeScope?.id === scopeId) {
1567
+ activeScope = null;
1568
+ recentSignatures.clear();
1569
+ detachListeners();
1570
+ }
1571
+ }
1572
+ function attachListeners() {
1573
+ if (isListening || typeof window === 'undefined') {
1574
+ return;
1575
+ }
1576
+ window.addEventListener('error', handleWindowError);
1577
+ window.addEventListener('unhandledrejection', handleUnhandledRejection);
1578
+ isListening = true;
1579
+ }
1580
+ function detachListeners() {
1581
+ if (!isListening || typeof window === 'undefined') {
1582
+ return;
1583
+ }
1584
+ window.removeEventListener('error', handleWindowError);
1585
+ window.removeEventListener('unhandledrejection', handleUnhandledRejection);
1586
+ isListening = false;
1587
+ }
1588
+ function handleWindowError(event) {
1589
+ const normalized = normalizeError(event.error || event.message || 'Unhandled browser error', 'UNHANDLED_ERROR');
1590
+ reportToActiveScopes(normalized, {
1591
+ event_type: 'error',
1592
+ filename: event.filename,
1593
+ lineno: event.lineno,
1594
+ colno: event.colno,
1595
+ });
1596
+ }
1597
+ function handleUnhandledRejection(event) {
1598
+ const normalized = normalizeError(event.reason || 'Unhandled promise rejection', 'UNHANDLED_REJECTION');
1599
+ reportToActiveScopes(normalized, {
1600
+ event_type: 'unhandledrejection',
1601
+ });
1602
+ }
1603
+ function reportToActiveScopes(error, eventContext) {
1604
+ if (!activeScope) {
1605
+ return;
1606
+ }
1607
+ if (!canReport(error, eventContext.event_type)) {
1608
+ return;
1609
+ }
1610
+ try {
1611
+ const context = activeScope.getContext();
1612
+ getErrorImage(activeScope.orgId, {
1613
+ message: `${error.name}: ${error.message}`,
1614
+ code: error.code,
1615
+ req_id: context.reqId,
1616
+ context: {
1617
+ ...eventContext,
1618
+ checkout_id: context.checkoutId,
1619
+ order_id: context.orderId,
1620
+ price_id: context.priceId,
1621
+ checkout_state: context.state,
1622
+ payment_method: context.paymentMethod,
1623
+ stack: error.stack,
1624
+ page_url: getPageUrl(),
1625
+ },
1626
+ });
1627
+ }
1628
+ catch {
1629
+ // Telemetry must never affect checkout behavior.
1630
+ }
1631
+ }
1632
+ function canReport(error, eventType) {
1633
+ const now = Date.now();
1634
+ pruneExpiredSignatures(now);
1635
+ const signature = buildSignature(error, eventType);
1636
+ const previousReportTime = recentSignatures.get(signature);
1637
+ if (typeof previousReportTime === 'number' &&
1638
+ now - previousReportTime < DEDUPE_WINDOW_MS) {
1639
+ return false;
1640
+ }
1641
+ recentSignatures.set(signature, now);
1642
+ return true;
1643
+ }
1644
+ function pruneExpiredSignatures(now) {
1645
+ recentSignatures.forEach((timestamp, signature) => {
1646
+ if (now - timestamp >= DEDUPE_WINDOW_MS) {
1647
+ recentSignatures.delete(signature);
1648
+ }
1649
+ });
1650
+ }
1651
+ function buildSignature(error, eventType) {
1652
+ const firstStackLine = error.stack?.split('\n')[0] || '';
1653
+ return [
1654
+ eventType || '',
1655
+ error.code || '',
1656
+ error.message || '',
1657
+ firstStackLine,
1658
+ ].join('|');
1659
+ }
1660
+ function normalizeError(reason, fallbackCode) {
1661
+ if (reason instanceof Error) {
1662
+ return {
1663
+ name: reason.name || 'Error',
1664
+ message: reason.message || 'Unknown error',
1665
+ code: getErrorCode(reason, fallbackCode),
1666
+ stack: reason.stack,
1667
+ };
1668
+ }
1669
+ if (typeof reason === 'object' && reason !== null) {
1670
+ const record = reason;
1671
+ return {
1672
+ name: toSafeString(record.name) || 'Error',
1673
+ message: toSafeString(record.message) || safeStringify(reason),
1674
+ code: toSafeString(record.code) || fallbackCode,
1675
+ stack: toSafeString(record.stack),
1676
+ };
1677
+ }
1678
+ return {
1679
+ name: 'Error',
1680
+ message: toSafeString(reason) || 'Unknown error',
1681
+ code: fallbackCode,
1682
+ };
1683
+ }
1684
+ function getErrorCode(error, fallbackCode) {
1685
+ const record = error;
1686
+ return typeof record.code === 'string' && record.code
1687
+ ? record.code
1688
+ : fallbackCode;
1689
+ }
1690
+ function toSafeString(value) {
1691
+ if (typeof value === 'string') {
1692
+ return value;
1693
+ }
1694
+ if (typeof value === 'number' || typeof value === 'boolean') {
1695
+ return String(value);
1696
+ }
1697
+ return '';
1698
+ }
1699
+ function safeStringify(value) {
1700
+ try {
1701
+ return JSON.stringify(value);
1702
+ }
1703
+ catch {
1704
+ return 'Unserializable error';
1705
+ }
1706
+ }
1707
+ function getPageUrl() {
1708
+ if (typeof window === 'undefined' || !window.location) {
1709
+ return '';
1710
+ }
1711
+ const location = window.location;
1712
+ if (location.origin && location.pathname) {
1713
+ return `${location.origin}${location.pathname}`;
1714
+ }
1715
+ return (location.href || '').split(/[?#]/)[0];
1716
+ }
1717
+
1502
1718
  /**
1503
1719
  * @fileoverview Checkout instance manager for Funnefox SDK
1504
1720
  */
@@ -1508,6 +1724,8 @@
1508
1724
  this.counter = 0;
1509
1725
  this.cachedSessionResponse = null;
1510
1726
  this.cardSessionFieldConfig = {};
1727
+ this.isTelemetryEnabled = false;
1728
+ this.telemetryCleanup = null;
1511
1729
  this.handleInputChange = (inputName, error) => {
1512
1730
  this.emit(EVENTS.INPUT_ERROR, { name: inputName, error });
1513
1731
  };
@@ -1670,6 +1888,7 @@
1670
1888
  await this.createSession();
1671
1889
  await this._initializePrimerCheckout();
1672
1890
  this._setState('ready');
1891
+ this.startUnhandledTelemetry();
1673
1892
  this.checkoutConfig?.onInitialized?.();
1674
1893
  return this;
1675
1894
  }
@@ -1744,6 +1963,8 @@
1744
1963
  sessionResponse = await sessionRequest;
1745
1964
  }
1746
1965
  this.cachedSessionResponse = sessionResponse;
1966
+ this.isTelemetryEnabled =
1967
+ !!sessionResponse.data?.sdk_telemetry_enabled || true;
1747
1968
  this.isCollectingApplePayEmail =
1748
1969
  !!sessionResponse.data?.collect_apple_pay_email;
1749
1970
  this.applySessionCardFieldConfig(sessionResponse);
@@ -2028,6 +2249,7 @@
2028
2249
  if (this.isDestroyed)
2029
2250
  return;
2030
2251
  try {
2252
+ this.stopUnhandledTelemetry();
2031
2253
  CheckoutInstance.sessionCache.clear();
2032
2254
  await this.primerWrapper.destroy();
2033
2255
  this._setState('destroyed');
@@ -2202,6 +2424,7 @@
2202
2424
  }
2203
2425
  await this.primerWrapper.initializeHeadlessCheckout(this.clientToken, checkoutOptions, method);
2204
2426
  const methodInterface = await this.primerWrapper.initMethod(method, element, methodOptions);
2427
+ this.startUnhandledTelemetry(method);
2205
2428
  return {
2206
2429
  ...methodInterface,
2207
2430
  destroy: async () => {
@@ -2210,25 +2433,34 @@
2210
2433
  },
2211
2434
  };
2212
2435
  }
2213
- }
2214
- CheckoutInstance.sessionCache = new Map();
2215
-
2216
- function getErrorImage(orgId, options) {
2217
- const params = new URLSearchParams({
2218
- message: options.message,
2219
- code: options.code,
2220
- timestamp: Date.now().toString(),
2221
- sdk_version: SDK_VERSION,
2222
- });
2223
- if (options.req_id) {
2224
- params.append('req_id', options.req_id);
2436
+ startUnhandledTelemetry(paymentMethod) {
2437
+ if (paymentMethod) {
2438
+ this.telemetryPaymentMethod = paymentMethod;
2439
+ }
2440
+ if (!this.isTelemetryEnabled || this.telemetryCleanup) {
2441
+ return;
2442
+ }
2443
+ this.telemetryCleanup = startUnhandledErrorTelemetry({
2444
+ id: this.id,
2445
+ orgId: this.orgId,
2446
+ enabled: this.isTelemetryEnabled,
2447
+ getContext: () => ({
2448
+ checkoutId: this.id,
2449
+ orderId: this.orderId,
2450
+ priceId: this.checkoutConfig.priceId,
2451
+ state: this.state,
2452
+ paymentMethod: this.telemetryPaymentMethod,
2453
+ reqId: this.cachedSessionResponse?.req_id,
2454
+ }),
2455
+ });
2456
+ }
2457
+ stopUnhandledTelemetry() {
2458
+ this.telemetryCleanup?.();
2459
+ this.telemetryCleanup = null;
2460
+ this.telemetryPaymentMethod = undefined;
2225
2461
  }
2226
- const url = `https://billing.funnelfox.com/sdk_report/${encodeURIComponent(orgId)}/crash?${params.toString()}`;
2227
- const img = new Image();
2228
- img.src = url;
2229
- img.style.display = 'none';
2230
- document.body.appendChild(img);
2231
2462
  }
2463
+ CheckoutInstance.sessionCache = new Map();
2232
2464
 
2233
2465
  /**
2234
2466
  * @fileoverview Public API with configuration and orchestration logic