@flopay/js 1.3.4 → 1.4.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/dist/index.mjs CHANGED
@@ -1,8 +1,15 @@
1
1
  // src/load.ts
2
- import { FloPayError as FloPayError6 } from "@flopay/shared";
2
+ import { FloPayError as FloPayError6, resolveBillingApiUrl as resolveBillingApiUrl3, SDK_VERSION as SDK_VERSION4 } from "@flopay/shared";
3
3
 
4
4
  // src/stripe-adapter.ts
5
+ import { loadStripe } from "@stripe/stripe-js";
5
6
  import { FloPayError, isSetupIntentClientSecret } from "@flopay/shared";
7
+
8
+ // src/card-three-ds.ts
9
+ var CARD_THREE_DS_ATTEMPT_TTL_MS = 15 * 6e4;
10
+ var MAX_CARD_THREE_DS_ATTEMPTS = 32;
11
+
12
+ // src/stripe-adapter.ts
6
13
  function toStripeElementType(type) {
7
14
  const map = {
8
15
  payment: "payment",
@@ -25,6 +32,9 @@ function toStripeAppearanceTheme(theme) {
25
32
  return "stripe";
26
33
  }
27
34
  }
35
+ function cardThreeDsNow() {
36
+ return globalThis.performance?.now() ?? Date.now();
37
+ }
28
38
  function wrapStripeElement(stripeElement) {
29
39
  const el = stripeElement;
30
40
  return {
@@ -48,11 +58,31 @@ function wrapStripeElement(stripeElement) {
48
58
  }
49
59
  };
50
60
  }
61
+ function toStripeBillingDetails(billing) {
62
+ return {
63
+ billing_details: {
64
+ ...billing.email ? { email: billing.email } : {},
65
+ ...billing.name ? { name: billing.name } : {},
66
+ ...billing.address ? {
67
+ address: {
68
+ ...billing.address.country ? { country: billing.address.country } : {},
69
+ ...billing.address.postal_code ? { postal_code: billing.address.postal_code } : {},
70
+ ...billing.address.city ? { city: billing.address.city } : {},
71
+ ...billing.address.line1 ? { line1: billing.address.line1 } : {},
72
+ ...billing.address.line2 ? { line2: billing.address.line2 } : {},
73
+ ...billing.address.state ? { state: billing.address.state } : {}
74
+ }
75
+ } : {}
76
+ }
77
+ };
78
+ }
51
79
  var StripeAdapter = class {
52
80
  constructor() {
53
81
  this.name = "stripe";
54
82
  this.stripe = null;
55
83
  this.elements = null;
84
+ this.pendingCardThreeDsAttempts = /* @__PURE__ */ new Map();
85
+ this.cardThreeDsAttemptSequence = 0;
56
86
  // Serialized appearance currently applied to `this.elements`. Used to detect
57
87
  // when consumers swap themes mid-session so we can live-update the Stripe
58
88
  // Elements group instead of returning a stale-styled cache. `null` while no
@@ -63,16 +93,6 @@ var StripeAdapter = class {
63
93
  if (typeof window === "undefined") {
64
94
  return;
65
95
  }
66
- let loadStripe;
67
- try {
68
- ({ loadStripe } = await import("@stripe/stripe-js"));
69
- } catch {
70
- throw new FloPayError(
71
- "Failed to load @stripe/stripe-js. Reinstall @flopay/js.",
72
- "api_error",
73
- { code: "StripeJsLoadFailed" }
74
- );
75
- }
76
96
  const stripe = await loadStripe(config.publishableKey, {
77
97
  locale: config.locale ?? "auto"
78
98
  });
@@ -184,22 +204,7 @@ var StripeAdapter = class {
184
204
  };
185
205
  }
186
206
  const cardNumberEl = this.elements.getElement("cardNumber");
187
- const stripeBilling = billingDetails ? {
188
- billing_details: {
189
- ...billingDetails.email ? { email: billingDetails.email } : {},
190
- ...billingDetails.name ? { name: billingDetails.name } : {},
191
- ...billingDetails.address ? {
192
- address: {
193
- ...billingDetails.address.country ? { country: billingDetails.address.country } : {},
194
- ...billingDetails.address.postal_code ? { postal_code: billingDetails.address.postal_code } : {},
195
- ...billingDetails.address.city ? { city: billingDetails.address.city } : {},
196
- ...billingDetails.address.line1 ? { line1: billingDetails.address.line1 } : {},
197
- ...billingDetails.address.line2 ? { line2: billingDetails.address.line2 } : {},
198
- ...billingDetails.address.state ? { state: billingDetails.address.state } : {}
199
- }
200
- } : {}
201
- }
202
- } : {};
207
+ const stripeBilling = billingDetails ? toStripeBillingDetails(billingDetails) : {};
203
208
  const { error, paymentMethod } = cardNumberEl ? await this.stripe.createPaymentMethod({
204
209
  type: "card",
205
210
  card: cardNumberEl,
@@ -233,41 +238,84 @@ var StripeAdapter = class {
233
238
  { payment_method: params.paymentMethodId }
234
239
  );
235
240
  if (setupError) {
236
- return {
241
+ return this.withCardThreeDsLifecycle(params, {
237
242
  status: "failed",
238
243
  error: new FloPayError(
239
244
  setupError.message ?? "Payment failed",
240
245
  "api_error",
241
246
  { code: setupError.code, declineCode: setupError.decline_code }
242
247
  )
243
- };
248
+ });
244
249
  }
245
- return {
250
+ return this.withCardThreeDsLifecycle(params, {
246
251
  status: setupIntent?.status ?? "failed",
247
252
  paymentIntentId: setupIntent?.id,
248
253
  paymentMethodId: this.extractPaymentMethodId(setupIntent?.payment_method)
249
- };
254
+ });
250
255
  }
251
256
  const { error, paymentIntent } = await this.stripe.confirmCardPayment(
252
257
  params.clientSecret,
253
258
  { payment_method: params.paymentMethodId }
254
259
  );
255
260
  if (error) {
256
- return {
261
+ return this.withCardThreeDsLifecycle(params, {
257
262
  status: "failed",
258
263
  error: new FloPayError(
259
264
  error.message ?? "Payment failed",
260
265
  "api_error",
261
266
  { code: error.code, declineCode: error.decline_code }
262
267
  )
263
- };
268
+ });
264
269
  }
265
- return {
270
+ return this.withCardThreeDsLifecycle(params, {
266
271
  status: paymentIntent?.status ?? "failed",
267
272
  paymentIntentId: paymentIntent?.id,
268
273
  paymentMethodId: this.extractPaymentMethodId(paymentIntent?.payment_method)
274
+ });
275
+ }
276
+ /**
277
+ * Bind provider-observed 3DS milestones to the exact confirmation context.
278
+ * The sensitive client secret/payment method pair stays only in this private,
279
+ * in-memory key; callers and telemetry receive an unrelated opaque id.
280
+ */
281
+ withCardThreeDsLifecycle(params, result) {
282
+ const contextKey = `${params.clientSecret}\0${params.paymentMethodId}`;
283
+ const now = cardThreeDsNow();
284
+ this.pruneExpiredCardThreeDsAttempts(now);
285
+ if (result.status === "requires_action") {
286
+ let attempt2 = this.pendingCardThreeDsAttempts.get(contextKey);
287
+ if (!attempt2) {
288
+ while (this.pendingCardThreeDsAttempts.size >= MAX_CARD_THREE_DS_ATTEMPTS) {
289
+ const oldestContextKey = this.pendingCardThreeDsAttempts.keys().next().value;
290
+ if (oldestContextKey === void 0) break;
291
+ this.pendingCardThreeDsAttempts.delete(oldestContextKey);
292
+ }
293
+ attempt2 = {
294
+ attemptId: `card_3ds_${this.cardThreeDsAttemptSequence++}`,
295
+ startedAt: now
296
+ };
297
+ this.pendingCardThreeDsAttempts.set(contextKey, attempt2);
298
+ }
299
+ return { ...result, threeDs: { attemptId: attempt2.attemptId, status: "handoff" } };
300
+ }
301
+ const attempt = this.pendingCardThreeDsAttempts.get(contextKey);
302
+ if (!attempt) return result;
303
+ this.pendingCardThreeDsAttempts.delete(contextKey);
304
+ return {
305
+ ...result,
306
+ threeDs: {
307
+ attemptId: attempt.attemptId,
308
+ status: result.error || result.status === "failed" ? "failed" : "returned"
309
+ }
269
310
  };
270
311
  }
312
+ pruneExpiredCardThreeDsAttempts(now) {
313
+ for (const [contextKey, attempt] of this.pendingCardThreeDsAttempts) {
314
+ if (now - attempt.startedAt >= CARD_THREE_DS_ATTEMPT_TTL_MS) {
315
+ this.pendingCardThreeDsAttempts.delete(contextKey);
316
+ }
317
+ }
318
+ }
271
319
  async confirmPayment(params) {
272
320
  if (!this.stripe || !this.elements) {
273
321
  throw new FloPayError(
@@ -276,22 +324,7 @@ var StripeAdapter = class {
276
324
  );
277
325
  }
278
326
  const billing = params.billingDetails;
279
- const paymentMethodData = billing ? {
280
- billing_details: {
281
- ...billing.email ? { email: billing.email } : {},
282
- ...billing.name ? { name: billing.name } : {},
283
- ...billing.address ? {
284
- address: {
285
- ...billing.address.country ? { country: billing.address.country } : {},
286
- ...billing.address.postal_code ? { postal_code: billing.address.postal_code } : {},
287
- ...billing.address.city ? { city: billing.address.city } : {},
288
- ...billing.address.line1 ? { line1: billing.address.line1 } : {},
289
- ...billing.address.line2 ? { line2: billing.address.line2 } : {},
290
- ...billing.address.state ? { state: billing.address.state } : {}
291
- }
292
- } : {}
293
- }
294
- } : void 0;
327
+ const paymentMethodData = billing ? toStripeBillingDetails(billing) : void 0;
295
328
  const { error, paymentIntent } = await this.stripe.confirmPayment({
296
329
  elements: this.elements,
297
330
  clientSecret: params.clientSecret,
@@ -454,6 +487,7 @@ var StripeAdapter = class {
454
487
  return this.stripe.elements(elementsOptions);
455
488
  }
456
489
  destroy() {
490
+ this.pendingCardThreeDsAttempts.clear();
457
491
  this.elements = null;
458
492
  this.appliedAppearanceKey = null;
459
493
  this.stripe = null;
@@ -461,7 +495,7 @@ var StripeAdapter = class {
461
495
  };
462
496
 
463
497
  // src/flopay.ts
464
- import { FloPayError as FloPayError5, resolveBillingApiUrl } from "@flopay/shared";
498
+ import { FloPayError as FloPayError5, resolveBillingApiUrl as resolveBillingApiUrl2, SDK_VERSION as SDK_VERSION3 } from "@flopay/shared";
465
499
 
466
500
  // src/elements.ts
467
501
  import "@flopay/shared";
@@ -525,6 +559,19 @@ import {
525
559
  resolveSessionCurrency
526
560
  } from "@flopay/shared";
527
561
 
562
+ // src/api-error.ts
563
+ function readErrorString(value) {
564
+ return typeof value === "string" && value.trim() ? value : void 0;
565
+ }
566
+ function readErrorMessage(value) {
567
+ if (typeof value === "string") return value.trim() ? value : void 0;
568
+ if (Array.isArray(value)) {
569
+ const joined = value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).join("; ");
570
+ return joined || void 0;
571
+ }
572
+ return void 0;
573
+ }
574
+
528
575
  // src/session-display-cache.ts
529
576
  var STORAGE_KEY_PREFIX = "flopay_session_display:";
530
577
  var DEFAULT_TTL_MS = 60 * 60 * 1e3;
@@ -591,6 +638,341 @@ function clearSessionDisplayData(sessionId) {
591
638
  }
592
639
  }
593
640
 
641
+ // src/telemetry-reporter.ts
642
+ import {
643
+ buildTelemetryErrorEvent,
644
+ buildTelemetryLogEvent,
645
+ buildTelemetryPerformanceEvent,
646
+ buildTelemetryTerminalEvent,
647
+ serializeTelemetryBatch,
648
+ TELEMETRY_MAX_BATCH_BYTES
649
+ } from "@flopay/shared";
650
+ var TELEMETRY_PATH = "/v1/sdk-telemetry/events";
651
+ var MAX_BATCH_SIZE = 16;
652
+ var MAX_QUEUE_SIZE = 64;
653
+ var UPLOAD_TIMEOUT_MS = 1500;
654
+ var ERROR_DEDUPLICATION_WINDOW_MS = 1e3;
655
+ var MAX_REPORTED_FAILURES = 64;
656
+ var DEDUPLICATION_EVENT_ID = "00000000-0000-4000-8000-000000000000";
657
+ var EVENT_BUDGETS = {
658
+ technical_error: 8,
659
+ lifecycle: 32,
660
+ expected_outcome: 32,
661
+ performance: 24
662
+ };
663
+ function createUuidV4() {
664
+ try {
665
+ return globalThis.crypto.randomUUID();
666
+ } catch {
667
+ const bytes = new Uint8Array(16);
668
+ try {
669
+ globalThis.crypto.getRandomValues(bytes);
670
+ } catch {
671
+ for (let index = 0; index < bytes.length; index += 1) {
672
+ bytes[index] = Math.floor(Math.random() * 256);
673
+ }
674
+ }
675
+ bytes[6] = bytes[6] & 15 | 64;
676
+ bytes[8] = bytes[8] & 63 | 128;
677
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
678
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
679
+ }
680
+ }
681
+ function bodyByteLength(body) {
682
+ try {
683
+ return new TextEncoder().encode(body).byteLength;
684
+ } catch {
685
+ return body.length;
686
+ }
687
+ }
688
+ function failureDeduplicationKey(event) {
689
+ return JSON.stringify([
690
+ event.code,
691
+ event.stage,
692
+ event.provider,
693
+ event.attempt,
694
+ event.statusClass,
695
+ event.requestCategory,
696
+ event.paymentMethodCategory,
697
+ event.checkoutMode,
698
+ event.layout
699
+ ]);
700
+ }
701
+ function telemetryNow() {
702
+ return globalThis.performance?.now() ?? 0;
703
+ }
704
+ var TelemetryReporter = class {
705
+ constructor(options) {
706
+ this.ingestionDisabled = false;
707
+ this.queue = [];
708
+ this.sequence = 0;
709
+ this.flushTimer = null;
710
+ this.flushInFlight = null;
711
+ this.reportedFailures = /* @__PURE__ */ new Map();
712
+ this.checkoutContext = {};
713
+ this.checkoutStartedAt = null;
714
+ this.destroyed = false;
715
+ this.pageExitHandler = () => {
716
+ this.drainQueue();
717
+ };
718
+ this.visibilityHandler = () => {
719
+ if (document.visibilityState === "hidden") void this.flush();
720
+ };
721
+ this.eventCounts = {
722
+ technical_error: 0,
723
+ lifecycle: 0,
724
+ expected_outcome: 0,
725
+ performance: 0
726
+ };
727
+ this.endpoint = `${options.billingApiUrl.replace(/\/+$/, "")}${TELEMETRY_PATH}`;
728
+ this.sdkPackage = options.sdkPackage ?? "@flopay/js";
729
+ this.sdkVersion = options.sdkVersion;
730
+ this.correlationId = createUuidV4();
731
+ this.merchantEnabled = options.enabled !== false;
732
+ this.clock = options.clock ?? telemetryNow;
733
+ this.browserTransportAvailable = typeof window !== "undefined" && typeof document !== "undefined";
734
+ if (this.browserTransportAvailable) {
735
+ window.addEventListener("pagehide", this.pageExitHandler);
736
+ document.addEventListener("visibilitychange", this.visibilityHandler);
737
+ }
738
+ }
739
+ log(input) {
740
+ if (!this.canCollect()) return;
741
+ this.enqueue(buildTelemetryLogEvent({
742
+ ...this.checkoutContext,
743
+ ...input,
744
+ eventId: createUuidV4(),
745
+ sequence: this.sequence++
746
+ }));
747
+ }
748
+ error(input) {
749
+ if (!this.canCollect()) return;
750
+ const normalizedFailure = buildTelemetryErrorEvent({
751
+ ...this.checkoutContext,
752
+ ...input,
753
+ eventId: DEDUPLICATION_EVENT_ID,
754
+ sequence: 0
755
+ });
756
+ const deduplicationKey = failureDeduplicationKey(normalizedFailure);
757
+ const now = this.now();
758
+ this.pruneReportedFailures(now);
759
+ const previouslyReportedAt = this.reportedFailures.get(deduplicationKey);
760
+ if (previouslyReportedAt !== void 0 && now >= previouslyReportedAt && now - previouslyReportedAt < ERROR_DEDUPLICATION_WINDOW_MS) {
761
+ this.log({
762
+ name: "operation.deduplicated",
763
+ stage: input.stage,
764
+ provider: input.provider,
765
+ paymentMethodCategory: input.paymentMethodCategory,
766
+ attempt: input.attempt
767
+ });
768
+ return;
769
+ }
770
+ this.rememberReportedFailure(deduplicationKey, now);
771
+ this.enqueue(buildTelemetryErrorEvent({
772
+ ...this.checkoutContext,
773
+ ...input,
774
+ eventId: createUuidV4(),
775
+ sequence: this.sequence++
776
+ }));
777
+ }
778
+ performance(input) {
779
+ if (!this.canCollect()) return;
780
+ this.enqueue(buildTelemetryPerformanceEvent({
781
+ ...this.checkoutContext,
782
+ ...input,
783
+ eventId: createUuidV4(),
784
+ sequence: this.sequence++
785
+ }));
786
+ }
787
+ terminal(input) {
788
+ if (!this.canCollect()) return;
789
+ this.enqueue(buildTelemetryTerminalEvent({
790
+ ...this.checkoutContext,
791
+ ...input,
792
+ eventId: createUuidV4(),
793
+ sequence: this.sequence++
794
+ }));
795
+ if (input.outcome !== "action_required" && this.checkoutStartedAt !== null) {
796
+ const checkoutStartedAt = this.checkoutStartedAt;
797
+ this.checkoutStartedAt = null;
798
+ this.performance({
799
+ stage: "total_journey",
800
+ durationMs: Math.max(0, this.now() - checkoutStartedAt),
801
+ durationMode: "total",
802
+ provider: input.provider,
803
+ paymentMethodCategory: input.paymentMethodCategory
804
+ });
805
+ }
806
+ }
807
+ /** @internal Read the reporter's monotonic clock without Resource Timing. */
808
+ now() {
809
+ try {
810
+ return this.clock();
811
+ } catch {
812
+ return telemetryNow();
813
+ }
814
+ }
815
+ pruneReportedFailures(now) {
816
+ for (const [key, reportedAt] of this.reportedFailures) {
817
+ if (now < reportedAt || now - reportedAt >= ERROR_DEDUPLICATION_WINDOW_MS) {
818
+ this.reportedFailures.delete(key);
819
+ }
820
+ }
821
+ }
822
+ rememberReportedFailure(key, reportedAt) {
823
+ while (this.reportedFailures.size >= MAX_REPORTED_FAILURES) {
824
+ const oldest = this.reportedFailures.keys().next();
825
+ if (oldest.done) break;
826
+ this.reportedFailures.delete(oldest.value);
827
+ }
828
+ this.reportedFailures.set(key, reportedAt);
829
+ }
830
+ /** @internal Add closed checkout dimensions to subsequent SDK events. */
831
+ setCheckoutContext(context) {
832
+ this.checkoutContext = {
833
+ checkoutMode: context.checkoutMode,
834
+ layout: context.layout
835
+ };
836
+ }
837
+ /** @internal Start a fresh checkout budget, dedupe window, and total span. */
838
+ beginCheckout(context = {}) {
839
+ if (!this.canCollect()) return 0;
840
+ this.drainQueue();
841
+ this.setCheckoutContext(context);
842
+ this.sequence = 0;
843
+ this.reportedFailures.clear();
844
+ this.eventCounts = {
845
+ technical_error: 0,
846
+ lifecycle: 0,
847
+ expected_outcome: 0,
848
+ performance: 0
849
+ };
850
+ this.checkoutStartedAt = this.now();
851
+ return this.checkoutStartedAt;
852
+ }
853
+ enqueue(event) {
854
+ if (!this.canCollect()) return;
855
+ if (this.queue.length >= MAX_QUEUE_SIZE || this.eventCounts[event.class] >= EVENT_BUDGETS[event.class]) return;
856
+ this.eventCounts[event.class] += 1;
857
+ this.queue.push(event);
858
+ if (this.queue.length >= MAX_BATCH_SIZE) {
859
+ void this.flush();
860
+ return;
861
+ }
862
+ this.scheduleFlush();
863
+ }
864
+ canCollect() {
865
+ return this.browserTransportAvailable && this.merchantEnabled && !this.ingestionDisabled && !this.destroyed;
866
+ }
867
+ /** Flush one bounded batch. Failures are intentionally dropped. */
868
+ async flush() {
869
+ if (this.flushInFlight) return this.flushInFlight;
870
+ if (!this.browserTransportAvailable || this.destroyed || this.ingestionDisabled || this.queue.length === 0) return;
871
+ this.clearFlushTimer();
872
+ const events = this.queue.splice(0, MAX_BATCH_SIZE);
873
+ this.flushInFlight = this.sendBatch(events).finally(() => {
874
+ this.flushInFlight = null;
875
+ if (this.queue.length > 0) this.scheduleFlush();
876
+ });
877
+ return this.flushInFlight;
878
+ }
879
+ /** Flush pending work and detach browser lifecycle listeners. */
880
+ destroy() {
881
+ if (this.destroyed) return;
882
+ this.drainQueue();
883
+ this.destroyed = true;
884
+ this.clearFlushTimer();
885
+ if (this.browserTransportAvailable) {
886
+ window.removeEventListener("pagehide", this.pageExitHandler);
887
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
888
+ }
889
+ this.queue.splice(0);
890
+ this.reportedFailures.clear();
891
+ }
892
+ /** Permanently honor a merchant opt-out and discard queued events. */
893
+ disable() {
894
+ this.merchantEnabled = false;
895
+ this.queue.splice(0);
896
+ this.reportedFailures.clear();
897
+ this.clearFlushTimer();
898
+ }
899
+ /** Start every bounded keepalive request synchronously before page teardown. */
900
+ drainQueue() {
901
+ if (!this.browserTransportAvailable || this.destroyed || this.ingestionDisabled || this.queue.length === 0) return;
902
+ this.clearFlushTimer();
903
+ while (this.queue.length > 0) {
904
+ const events = this.queue.splice(0, MAX_BATCH_SIZE);
905
+ void this.sendBatch(events);
906
+ }
907
+ }
908
+ async sendBatch(events) {
909
+ if (!this.browserTransportAvailable || this.ingestionDisabled) return;
910
+ const body = serializeTelemetryBatch(events, {
911
+ correlationId: this.correlationId,
912
+ sdkPackage: this.sdkPackage,
913
+ sdkVersion: this.sdkVersion,
914
+ batchId: createUuidV4()
915
+ });
916
+ if (bodyByteLength(body) > TELEMETRY_MAX_BATCH_BYTES) return;
917
+ const controller = typeof AbortController === "undefined" ? null : new AbortController();
918
+ let timeout = null;
919
+ try {
920
+ const request = fetch(this.endpoint, {
921
+ method: "POST",
922
+ headers: { "content-type": "text/plain;charset=UTF-8" },
923
+ body,
924
+ credentials: "omit",
925
+ keepalive: true,
926
+ referrerPolicy: "no-referrer",
927
+ signal: controller?.signal
928
+ }).then(async (response) => {
929
+ if (response.status !== 202) return null;
930
+ const payload = await response.json().catch(() => null);
931
+ return payload?.status === "disabled" ? "disabled" : null;
932
+ }).catch(() => null);
933
+ const expired = new Promise((resolve) => {
934
+ timeout = setTimeout(() => {
935
+ controller?.abort();
936
+ resolve(null);
937
+ }, UPLOAD_TIMEOUT_MS);
938
+ });
939
+ const status = await Promise.race([request, expired]);
940
+ if (status === "disabled") this.disableFromIngestion();
941
+ } catch {
942
+ } finally {
943
+ if (timeout) clearTimeout(timeout);
944
+ }
945
+ }
946
+ disableFromIngestion() {
947
+ this.ingestionDisabled = true;
948
+ this.queue.splice(0);
949
+ this.reportedFailures.clear();
950
+ this.clearFlushTimer();
951
+ }
952
+ scheduleFlush() {
953
+ if (this.flushTimer || this.destroyed || this.ingestionDisabled) return;
954
+ this.flushTimer = setTimeout(() => {
955
+ this.flushTimer = null;
956
+ void this.flush();
957
+ }, 0);
958
+ }
959
+ clearFlushTimer() {
960
+ if (!this.flushTimer) return;
961
+ clearTimeout(this.flushTimer);
962
+ this.flushTimer = null;
963
+ }
964
+ };
965
+ var TELEMETRY_REPORTER_FACTORY = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.reporter-factory.v1");
966
+ var telemetryGlobal = globalThis;
967
+ if (telemetryGlobal[TELEMETRY_REPORTER_FACTORY] === void 0) {
968
+ Object.defineProperty(telemetryGlobal, TELEMETRY_REPORTER_FACTORY, {
969
+ configurable: true,
970
+ enumerable: false,
971
+ writable: false,
972
+ value: (options) => new TelemetryReporter(options)
973
+ });
974
+ }
975
+
594
976
  // src/payment-api.ts
595
977
  var DEFAULT_PROCESSING_RETRY_AFTER_MS = 1e3;
596
978
  var MIN_PROCESSING_RETRY_AFTER_MS = 500;
@@ -600,18 +982,30 @@ var DEFAULT_ACCOUNT_SNAPSHOT_TIMEOUT_MS = 1e4;
600
982
  function isRecord(value) {
601
983
  return typeof value === "object" && value !== null;
602
984
  }
985
+ function telemetryStatusClass(status) {
986
+ if (status === void 0) return "network_error";
987
+ const statusClass = `${Math.floor(status / 100)}xx`;
988
+ return statusClass === "2xx" || statusClass === "3xx" || statusClass === "4xx" || statusClass === "5xx" ? statusClass : "unknown";
989
+ }
990
+ function telemetryFailure(error, fallbackCode) {
991
+ if (error instanceof Error && (error.name === "AbortError" || error instanceof FloPayError3 && error.code === "checkout_processing_timeout")) {
992
+ return { errorCode: "REQUEST_TIMEOUT", statusClass: "timeout" };
993
+ }
994
+ if (error instanceof TypeError) {
995
+ return { errorCode: "NETWORK_REQUEST_FAILED", statusClass: "network_error" };
996
+ }
997
+ return {
998
+ errorCode: fallbackCode,
999
+ statusClass: telemetryStatusClass(
1000
+ error instanceof FloPayError3 ? error.statusCode : void 0
1001
+ )
1002
+ };
1003
+ }
603
1004
  function readString(payload, key) {
604
- const value = payload?.[key];
605
- return typeof value === "string" && value.trim() ? value : void 0;
1005
+ return readErrorString(payload?.[key]);
606
1006
  }
607
1007
  function readMessage(payload, key) {
608
- const value = payload?.[key];
609
- if (typeof value === "string") return value.trim() ? value : void 0;
610
- if (Array.isArray(value)) {
611
- const joined = value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).join("; ");
612
- return joined || void 0;
613
- }
614
- return void 0;
1008
+ return readErrorMessage(payload?.[key]);
615
1009
  }
616
1010
  function readNumber(payload, key) {
617
1011
  const value = payload?.[key];
@@ -639,7 +1033,7 @@ async function buildApiErrorFromResponse(response, fallbackMessage) {
639
1033
  }
640
1034
  var NETWORK_RETRY_ATTEMPTS = 2;
641
1035
  var IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS = 2;
642
- async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEMPTS) {
1036
+ async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEMPTS, onRetry) {
643
1037
  let lastErr;
644
1038
  for (let attempt = 0; ; attempt++) {
645
1039
  try {
@@ -648,13 +1042,59 @@ async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEM
648
1042
  if (err instanceof Error && err.name === "AbortError") throw err;
649
1043
  lastErr = err;
650
1044
  if (attempt >= attempts) throw lastErr;
1045
+ try {
1046
+ onRetry?.(attempt + 1);
1047
+ } catch {
1048
+ }
651
1049
  await delay(150 * 2 ** attempt);
652
1050
  }
653
1051
  }
654
1052
  }
1053
+ function isPaymentApiTelemetryHooks(value) {
1054
+ return "now" in value || "onFirstByte" in value || "onSessionCreateFailure" in value || "onRetry" in value;
1055
+ }
655
1056
  var PaymentAPI = class {
656
- constructor(billingApiUrl) {
1057
+ constructor(billingApiUrl, telemetryOptionsOrHooks = {}) {
657
1058
  this.baseUrl = billingApiUrl.replace(/\/+$/, "");
1059
+ const hasInternalHooks = isPaymentApiTelemetryHooks(telemetryOptionsOrHooks);
1060
+ this.telemetryHooks = hasInternalHooks ? telemetryOptionsOrHooks : void 0;
1061
+ this.directTelemetry = hasInternalHooks || telemetryOptionsOrHooks.telemetry === false ? void 0 : new TelemetryReporter({
1062
+ billingApiUrl: this.baseUrl,
1063
+ sdkVersion: SDK_VERSION
1064
+ });
1065
+ }
1066
+ /** Dispose the reporter owned by direct public usage. Internal hooks are never disposed here. */
1067
+ destroy() {
1068
+ this.directTelemetry?.destroy();
1069
+ }
1070
+ reportDirectFailure(error, fallbackCode, stage, requestCategory, paymentMethodCategory = "unknown") {
1071
+ const failure = telemetryFailure(error, fallbackCode);
1072
+ this.directTelemetry?.error({
1073
+ ...failure,
1074
+ stage,
1075
+ requestCategory,
1076
+ paymentMethodCategory
1077
+ });
1078
+ }
1079
+ telemetryTimestamp() {
1080
+ try {
1081
+ return this.telemetryHooks?.now?.() ?? this.directTelemetry?.now() ?? telemetryNow();
1082
+ } catch {
1083
+ return telemetryNow();
1084
+ }
1085
+ }
1086
+ beginDirectTelemetryCheckout(checkoutSessionId) {
1087
+ if (!this.directTelemetry || this.directTelemetryCheckoutId === checkoutSessionId) return;
1088
+ this.directTelemetryCheckoutId = checkoutSessionId;
1089
+ this.directTelemetry.beginCheckout();
1090
+ }
1091
+ beginDirectTelemetryOperation() {
1092
+ if (!this.directTelemetry) return;
1093
+ this.directTelemetryCheckoutId = void 0;
1094
+ this.directTelemetry.beginCheckout();
1095
+ }
1096
+ adoptDirectTelemetryCheckout(checkoutSessionId) {
1097
+ if (checkoutSessionId) this.directTelemetryCheckoutId = checkoutSessionId;
658
1098
  }
659
1099
  /**
660
1100
  * Fetch a raw checkout session by ID.
@@ -666,17 +1106,77 @@ var PaymentAPI = class {
666
1106
  * Backends that don't yet enforce it ignore the extra header.
667
1107
  */
668
1108
  async getCheckoutSession(checkoutSessionId, nonce) {
1109
+ this.beginDirectTelemetryCheckout(checkoutSessionId);
1110
+ const requestStarted = this.telemetryTimestamp();
1111
+ this.directTelemetry?.log({
1112
+ name: "session.read.started",
1113
+ stage: "session_read",
1114
+ requestCategory: "session_read"
1115
+ });
669
1116
  const headers = { [FLO_SDK_VERSION_HEADER]: SDK_VERSION };
670
1117
  if (nonce) headers["x-checkout-session-token"] = nonce;
671
- const response = await fetchWithNetworkRetry(
672
- `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}`,
673
- { headers }
674
- );
675
- if (!response.ok) {
676
- throw await buildApiErrorFromResponse(response, "Failed to get checkout session");
1118
+ try {
1119
+ const response = await fetchWithNetworkRetry(
1120
+ `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}`,
1121
+ { headers },
1122
+ NETWORK_RETRY_ATTEMPTS,
1123
+ (attempt) => {
1124
+ this.telemetryHooks?.onRetry?.("session_read", attempt);
1125
+ this.directTelemetry?.log({
1126
+ name: "operation.retry",
1127
+ stage: "session_read",
1128
+ requestCategory: "session_read",
1129
+ attempt
1130
+ });
1131
+ }
1132
+ );
1133
+ const firstByteDuration = Math.max(0, this.telemetryTimestamp() - requestStarted);
1134
+ try {
1135
+ this.telemetryHooks?.onFirstByte?.(firstByteDuration);
1136
+ } catch {
1137
+ }
1138
+ const statusClass = `${Math.floor(response.status / 100)}xx`;
1139
+ this.directTelemetry?.log({
1140
+ name: "session.request.first_byte",
1141
+ stage: "session_first_byte",
1142
+ requestCategory: "session_read",
1143
+ statusClass
1144
+ });
1145
+ this.directTelemetry?.performance({
1146
+ stage: "session_first_byte",
1147
+ durationMs: firstByteDuration,
1148
+ durationMode: "machine",
1149
+ requestCategory: "session_read",
1150
+ statusClass
1151
+ });
1152
+ if (!response.ok) {
1153
+ throw await buildApiErrorFromResponse(response, "Failed to get checkout session");
1154
+ }
1155
+ const body = await response.json();
1156
+ this.directTelemetry?.log({
1157
+ name: "session.request.completed",
1158
+ stage: "session_complete",
1159
+ requestCategory: "session_read",
1160
+ statusClass
1161
+ });
1162
+ this.directTelemetry?.performance({
1163
+ stage: "session_complete",
1164
+ durationMs: Math.max(0, this.telemetryTimestamp() - requestStarted),
1165
+ durationMode: "machine",
1166
+ requestCategory: "session_read",
1167
+ statusClass
1168
+ });
1169
+ return { ...body, data: this.mergeCachedDisplayData(body.data) };
1170
+ } catch (error) {
1171
+ const statusCode = error instanceof FloPayError3 ? error.statusCode : void 0;
1172
+ this.directTelemetry?.error({
1173
+ errorCode: error instanceof FloPayError3 && error.code === "checkout_processing_timeout" ? "REQUEST_TIMEOUT" : "NETWORK_REQUEST_FAILED",
1174
+ stage: "session_read",
1175
+ requestCategory: "session_read",
1176
+ statusClass: statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
1177
+ });
1178
+ throw error;
677
1179
  }
678
- const body = await response.json();
679
- return { ...body, data: this.mergeCachedDisplayData(body.data) };
680
1180
  }
681
1181
  /**
682
1182
  * Stash display-only data for a session so subsequent fetches can fill in
@@ -732,17 +1232,55 @@ var PaymentAPI = class {
732
1232
  * backends, matched against the session's stored nonce).
733
1233
  */
734
1234
  async getVaultCapture(checkoutSessionId, nonce) {
1235
+ this.beginDirectTelemetryCheckout(checkoutSessionId);
1236
+ const startedAt = this.telemetryTimestamp();
1237
+ this.directTelemetry?.log({
1238
+ name: "vault.capture.requested",
1239
+ stage: "vault_request",
1240
+ requestCategory: "vault_capture",
1241
+ paymentMethodCategory: "card"
1242
+ });
735
1243
  const headers = { "Content-Type": "application/json" };
736
1244
  if (nonce) headers["x-checkout-session-token"] = nonce;
737
- const response = await fetchWithNetworkRetry(
738
- `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}/vault/capture`,
739
- { method: "POST", headers }
740
- );
741
- if (!response.ok) {
742
- throw await buildApiErrorFromResponse(response, "Failed to load the secure card form");
1245
+ try {
1246
+ const response = await fetchWithNetworkRetry(
1247
+ `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}/vault/capture`,
1248
+ { method: "POST", headers },
1249
+ NETWORK_RETRY_ATTEMPTS,
1250
+ (attempt) => {
1251
+ this.telemetryHooks?.onRetry?.("vault_capture", attempt);
1252
+ this.directTelemetry?.log({
1253
+ name: "operation.retry",
1254
+ stage: "vault_request",
1255
+ requestCategory: "vault_capture",
1256
+ paymentMethodCategory: "card",
1257
+ attempt
1258
+ });
1259
+ }
1260
+ );
1261
+ if (!response.ok) {
1262
+ throw await buildApiErrorFromResponse(response, "Failed to load the secure card form");
1263
+ }
1264
+ const block = await response.json();
1265
+ this.directTelemetry?.performance({
1266
+ stage: "vault_request",
1267
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1268
+ durationMode: "machine",
1269
+ requestCategory: "vault_capture",
1270
+ paymentMethodCategory: "card",
1271
+ statusClass: "2xx"
1272
+ });
1273
+ return this.toVaultBlock(block);
1274
+ } catch (error) {
1275
+ this.reportDirectFailure(
1276
+ error,
1277
+ "VAULT_LOAD_FAILED",
1278
+ "vault_request",
1279
+ "vault_capture",
1280
+ "card"
1281
+ );
1282
+ throw error;
743
1283
  }
744
- const block = await response.json();
745
- return this.toVaultBlock(block);
746
1284
  }
747
1285
  /**
748
1286
  * Fetch and normalize a checkout session.
@@ -778,26 +1316,88 @@ var PaymentAPI = class {
778
1316
  * future major version.
779
1317
  */
780
1318
  async processPayment(_userId, data, options) {
1319
+ this.beginDirectTelemetryCheckout(data.sessionId);
781
1320
  if (!data.nonce) {
1321
+ this.directTelemetry?.terminal({
1322
+ outcome: "validation_rejected",
1323
+ stage: "processing",
1324
+ requestCategory: "process_payment"
1325
+ });
782
1326
  throw new FloPayError3(
783
1327
  "processPayment requires `nonce` \u2014 pass the value returned from session creation.",
784
1328
  "validation_error",
785
1329
  { code: "MissingCheckoutSessionToken", param: "nonce" }
786
1330
  );
787
1331
  }
1332
+ const startedAt = this.telemetryTimestamp();
1333
+ this.directTelemetry?.log({
1334
+ name: "payment.processing.started",
1335
+ stage: "processing",
1336
+ requestCategory: "process_payment"
1337
+ });
788
1338
  const { nonce, ...processBody } = data;
789
- const response = await fetch(
790
- `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(data.sessionId)}/process`,
791
- {
792
- method: "POST",
793
- headers: {
794
- "Content-Type": "application/json",
795
- "x-checkout-session-token": nonce
796
- },
797
- body: JSON.stringify(processBody)
1339
+ let response;
1340
+ try {
1341
+ response = await fetch(
1342
+ `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(data.sessionId)}/process`,
1343
+ {
1344
+ method: "POST",
1345
+ headers: {
1346
+ "Content-Type": "application/json",
1347
+ "x-checkout-session-token": nonce
1348
+ },
1349
+ body: JSON.stringify(processBody)
1350
+ }
1351
+ );
1352
+ } catch (error) {
1353
+ this.reportDirectFailure(
1354
+ error,
1355
+ "PAYMENT_PROCESSING_FAILED",
1356
+ "processing",
1357
+ "process_payment"
1358
+ );
1359
+ throw error;
1360
+ }
1361
+ if (!response.ok && response.status !== 202) {
1362
+ this.directTelemetry?.error({
1363
+ errorCode: "PAYMENT_PROCESSING_FAILED",
1364
+ stage: "processing",
1365
+ requestCategory: "process_payment",
1366
+ statusClass: telemetryStatusClass(response.status)
1367
+ });
1368
+ return response;
1369
+ }
1370
+ try {
1371
+ const result = await this.resolveProcessResponse(
1372
+ response,
1373
+ data.sessionId,
1374
+ { ...options, nonce }
1375
+ );
1376
+ this.directTelemetry?.log({
1377
+ name: "payment.processing.completed",
1378
+ stage: "processing",
1379
+ requestCategory: "process_payment",
1380
+ statusClass: telemetryStatusClass(result.status)
1381
+ });
1382
+ this.directTelemetry?.performance({
1383
+ stage: "processing",
1384
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1385
+ durationMode: "machine",
1386
+ requestCategory: "process_payment",
1387
+ statusClass: telemetryStatusClass(result.status)
1388
+ });
1389
+ return result;
1390
+ } catch (error) {
1391
+ if (!(error instanceof FloPayError3 && error.code === "checkout_processing_timeout")) {
1392
+ this.reportDirectFailure(
1393
+ error,
1394
+ "PAYMENT_PROCESSING_FAILED",
1395
+ "processing",
1396
+ "process_payment"
1397
+ );
798
1398
  }
799
- );
800
- return this.resolveProcessResponse(response, data.sessionId, { ...options, nonce });
1399
+ throw error;
1400
+ }
801
1401
  }
802
1402
  /**
803
1403
  * Patch the buyer's account snapshot (email, name, billing address, AVS
@@ -821,6 +1421,13 @@ var PaymentAPI = class {
821
1421
  * AVS-protected charge to decline downstream.
822
1422
  */
823
1423
  async patchAccountSnapshot(sessionId, nonce, body, options) {
1424
+ this.beginDirectTelemetryCheckout(sessionId);
1425
+ const startedAt = this.telemetryTimestamp();
1426
+ this.directTelemetry?.log({
1427
+ name: "operation.state_transition",
1428
+ stage: "processing",
1429
+ requestCategory: "account_snapshot"
1430
+ });
824
1431
  const timeoutMs = options?.timeoutMs ?? DEFAULT_ACCOUNT_SNAPSHOT_TIMEOUT_MS;
825
1432
  const controller = new AbortController();
826
1433
  const onCallerAbort = () => controller.abort();
@@ -829,26 +1436,53 @@ var PaymentAPI = class {
829
1436
  else options.signal.addEventListener("abort", onCallerAbort, { once: true });
830
1437
  }
831
1438
  const timer = setTimeout(() => controller.abort(), timeoutMs);
832
- let response;
833
1439
  try {
834
- response = await fetchWithNetworkRetry(
835
- `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(sessionId)}/account`,
836
- {
837
- method: "PATCH",
838
- headers: {
839
- "Content-Type": "application/json",
840
- "x-checkout-session-token": nonce
1440
+ let response;
1441
+ try {
1442
+ response = await fetchWithNetworkRetry(
1443
+ `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(sessionId)}/account`,
1444
+ {
1445
+ method: "PATCH",
1446
+ headers: {
1447
+ "Content-Type": "application/json",
1448
+ "x-checkout-session-token": nonce
1449
+ },
1450
+ body: JSON.stringify(body),
1451
+ signal: controller.signal
841
1452
  },
842
- body: JSON.stringify(body),
843
- signal: controller.signal
844
- }
1453
+ NETWORK_RETRY_ATTEMPTS,
1454
+ (attempt) => {
1455
+ this.telemetryHooks?.onRetry?.("account_snapshot", attempt);
1456
+ this.directTelemetry?.log({
1457
+ name: "operation.retry",
1458
+ stage: "processing",
1459
+ requestCategory: "account_snapshot",
1460
+ attempt
1461
+ });
1462
+ }
1463
+ );
1464
+ } finally {
1465
+ clearTimeout(timer);
1466
+ options?.signal?.removeEventListener("abort", onCallerAbort);
1467
+ }
1468
+ if (!response.ok) {
1469
+ throw await buildApiErrorFromResponse(response, "Failed to persist account snapshot");
1470
+ }
1471
+ this.directTelemetry?.performance({
1472
+ stage: "processing",
1473
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1474
+ durationMode: "machine",
1475
+ requestCategory: "account_snapshot",
1476
+ statusClass: "2xx"
1477
+ });
1478
+ } catch (error) {
1479
+ this.reportDirectFailure(
1480
+ error,
1481
+ "NETWORK_REQUEST_FAILED",
1482
+ "processing",
1483
+ "account_snapshot"
845
1484
  );
846
- } finally {
847
- clearTimeout(timer);
848
- options?.signal?.removeEventListener("abort", onCallerAbort);
849
- }
850
- if (!response.ok) {
851
- throw await buildApiErrorFromResponse(response, "Failed to persist account snapshot");
1485
+ throw error;
852
1486
  }
853
1487
  }
854
1488
  /**
@@ -870,22 +1504,62 @@ var PaymentAPI = class {
870
1504
  * passing the concrete payment method id / type.
871
1505
  */
872
1506
  async createPaymentIntent(sessionId, email, paymentMethodType, options) {
1507
+ this.beginDirectTelemetryCheckout(sessionId);
1508
+ const startedAt = this.telemetryTimestamp();
1509
+ this.directTelemetry?.log({
1510
+ name: "payment.intent.started",
1511
+ stage: "processing",
1512
+ requestCategory: "intent_create"
1513
+ });
873
1514
  const headers = { "Content-Type": "application/json" };
874
1515
  if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
875
- return fetch(
876
- `${this.baseUrl}/v1/checkouts/payments/intents`,
877
- {
878
- method: "POST",
879
- headers,
880
- body: JSON.stringify({
881
- sessionId,
882
- email,
883
- paymentMethodType: paymentMethodType ?? null,
884
- isPaypal: options?.isPaypal ?? false
885
- }),
886
- signal: options?.signal
1516
+ try {
1517
+ const response = await fetch(
1518
+ `${this.baseUrl}/v1/checkouts/payments/intents`,
1519
+ {
1520
+ method: "POST",
1521
+ headers,
1522
+ body: JSON.stringify({
1523
+ sessionId,
1524
+ email,
1525
+ paymentMethodType: paymentMethodType ?? null,
1526
+ isPaypal: options?.isPaypal ?? false
1527
+ }),
1528
+ signal: options?.signal
1529
+ }
1530
+ );
1531
+ if (!response.ok) {
1532
+ this.directTelemetry?.error({
1533
+ errorCode: "PAYMENT_PROCESSING_FAILED",
1534
+ stage: "processing",
1535
+ requestCategory: "intent_create",
1536
+ statusClass: telemetryStatusClass(response.status)
1537
+ });
1538
+ return response;
887
1539
  }
888
- );
1540
+ this.directTelemetry?.log({
1541
+ name: "payment.intent.completed",
1542
+ stage: "processing",
1543
+ requestCategory: "intent_create",
1544
+ statusClass: telemetryStatusClass(response.status)
1545
+ });
1546
+ this.directTelemetry?.performance({
1547
+ stage: "processing",
1548
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1549
+ durationMode: "machine",
1550
+ requestCategory: "intent_create",
1551
+ statusClass: telemetryStatusClass(response.status)
1552
+ });
1553
+ return response;
1554
+ } catch (error) {
1555
+ this.reportDirectFailure(
1556
+ error,
1557
+ "PAYMENT_PROCESSING_FAILED",
1558
+ "processing",
1559
+ "intent_create"
1560
+ );
1561
+ throw error;
1562
+ }
889
1563
  }
890
1564
  /**
891
1565
  * Create a SetupIntent for saving payment methods.
@@ -894,23 +1568,71 @@ var PaymentAPI = class {
894
1568
  * required by post-#640 backends, ignored by earlier versions.
895
1569
  */
896
1570
  async createSetupIntent(sessionId, email, paymentMethodType, options) {
1571
+ this.beginDirectTelemetryCheckout(sessionId);
1572
+ const startedAt = this.telemetryTimestamp();
1573
+ this.directTelemetry?.log({
1574
+ name: "payment.intent.started",
1575
+ stage: "processing",
1576
+ requestCategory: "intent_create"
1577
+ });
897
1578
  const headers = { "Content-Type": "application/json" };
898
1579
  if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
899
- return fetch(
900
- `${this.baseUrl}/v1/checkouts/payments/setup-intents`,
901
- {
902
- method: "POST",
903
- headers,
904
- body: JSON.stringify({ sessionId, email, paymentMethodType }),
905
- signal: options?.signal
1580
+ try {
1581
+ const response = await fetch(
1582
+ `${this.baseUrl}/v1/checkouts/payments/setup-intents`,
1583
+ {
1584
+ method: "POST",
1585
+ headers,
1586
+ body: JSON.stringify({ sessionId, email, paymentMethodType }),
1587
+ signal: options?.signal
1588
+ }
1589
+ );
1590
+ if (!response.ok) {
1591
+ this.directTelemetry?.error({
1592
+ errorCode: "PAYMENT_PROCESSING_FAILED",
1593
+ stage: "processing",
1594
+ requestCategory: "intent_create",
1595
+ statusClass: telemetryStatusClass(response.status)
1596
+ });
1597
+ return response;
906
1598
  }
907
- );
1599
+ this.directTelemetry?.log({
1600
+ name: "payment.intent.completed",
1601
+ stage: "processing",
1602
+ requestCategory: "intent_create",
1603
+ statusClass: telemetryStatusClass(response.status)
1604
+ });
1605
+ this.directTelemetry?.performance({
1606
+ stage: "processing",
1607
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1608
+ durationMode: "machine",
1609
+ requestCategory: "intent_create",
1610
+ statusClass: telemetryStatusClass(response.status)
1611
+ });
1612
+ return response;
1613
+ } catch (error) {
1614
+ this.reportDirectFailure(
1615
+ error,
1616
+ "PAYMENT_PROCESSING_FAILED",
1617
+ "processing",
1618
+ "intent_create"
1619
+ );
1620
+ throw error;
1621
+ }
908
1622
  }
909
1623
  /**
910
1624
  * Fetch user's prior payments by email.
911
1625
  * Used to determine if saved card UX should be shown.
912
1626
  */
913
1627
  async getPaymentsByEmail(email, options) {
1628
+ this.beginDirectTelemetryOperation();
1629
+ const startedAt = this.telemetryTimestamp();
1630
+ this.directTelemetry?.log({
1631
+ name: "operation.recovery.started",
1632
+ stage: "recovery",
1633
+ requestCategory: "other",
1634
+ paymentMethodCategory: "saved"
1635
+ });
914
1636
  const page = options?.page ?? 1;
915
1637
  const limit = options?.limit ?? 1;
916
1638
  const params = new URLSearchParams({
@@ -920,18 +1642,43 @@ var PaymentAPI = class {
920
1642
  sortField: "createdAt",
921
1643
  sortDirection: "DESC"
922
1644
  });
923
- const response = await fetch(
924
- `${this.baseUrl}/v1/payments?${params.toString()}`,
925
- {
926
- method: "GET",
927
- signal: options?.signal,
928
- keepalive: true
1645
+ try {
1646
+ const response = await fetch(
1647
+ `${this.baseUrl}/v1/payments?${params.toString()}`,
1648
+ {
1649
+ method: "GET",
1650
+ signal: options?.signal,
1651
+ keepalive: true
1652
+ }
1653
+ );
1654
+ if (!response.ok) {
1655
+ throw new FloPayError3(
1656
+ "Failed to fetch payments",
1657
+ "api_error",
1658
+ { statusCode: response.status }
1659
+ );
929
1660
  }
930
- );
931
- if (!response.ok) {
932
- throw new FloPayError3("Failed to fetch payments", "api_error");
1661
+ const result = await response.json();
1662
+ this.directTelemetry?.log({
1663
+ name: "operation.recovery.completed",
1664
+ stage: "recovery",
1665
+ requestCategory: "other",
1666
+ paymentMethodCategory: "saved",
1667
+ statusClass: "2xx"
1668
+ });
1669
+ this.directTelemetry?.performance({
1670
+ stage: "recovery",
1671
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1672
+ durationMode: "machine",
1673
+ requestCategory: "other",
1674
+ paymentMethodCategory: "saved",
1675
+ statusClass: "2xx"
1676
+ });
1677
+ return result;
1678
+ } catch (error) {
1679
+ this.reportDirectFailure(error, "RECOVERY_FAILED", "recovery", "other", "saved");
1680
+ throw error;
933
1681
  }
934
- return response.json();
935
1682
  }
936
1683
  /**
937
1684
  * Create a checkout session AND return the full session data in one call.
@@ -941,6 +1688,56 @@ var PaymentAPI = class {
941
1688
  * Falls back to create + GET if the backend doesn't support `expand`.
942
1689
  */
943
1690
  async createAndFetchSession(params) {
1691
+ this.beginDirectTelemetryOperation();
1692
+ const startedAt = this.telemetryTimestamp();
1693
+ this.directTelemetry?.log({
1694
+ name: "session.create.started",
1695
+ stage: "session_create",
1696
+ requestCategory: "session_create"
1697
+ });
1698
+ try {
1699
+ const result = await this.createAndFetchSessionRequest(params, startedAt);
1700
+ this.adoptDirectTelemetryCheckout(result.data.session?.id);
1701
+ this.directTelemetry?.log({
1702
+ name: "session.request.completed",
1703
+ stage: "session_complete",
1704
+ requestCategory: "session_create",
1705
+ statusClass: "2xx"
1706
+ });
1707
+ this.directTelemetry?.performance({
1708
+ stage: "session_create",
1709
+ durationMs: this.telemetryTimestamp() - startedAt,
1710
+ durationMode: "machine",
1711
+ requestCategory: "session_create",
1712
+ statusClass: "2xx"
1713
+ });
1714
+ return result;
1715
+ } catch (error) {
1716
+ if (!(error instanceof FloPayError3 && error.code === "session_auto_completed")) {
1717
+ try {
1718
+ this.telemetryHooks?.onSessionCreateFailure?.(error);
1719
+ } catch {
1720
+ }
1721
+ }
1722
+ if (error instanceof FloPayError3 && error.type === "validation_error") {
1723
+ this.directTelemetry?.terminal({
1724
+ outcome: "validation_rejected",
1725
+ stage: "session_create",
1726
+ requestCategory: "session_create"
1727
+ });
1728
+ } else if (!(error instanceof FloPayError3 && error.code === "session_auto_completed")) {
1729
+ const statusCode = error instanceof FloPayError3 ? error.statusCode : void 0;
1730
+ this.directTelemetry?.error({
1731
+ errorCode: error instanceof Error && error.name === "AbortError" ? "REQUEST_TIMEOUT" : error instanceof TypeError ? "NETWORK_REQUEST_FAILED" : "CHECKOUT_SESSION_CREATE_FAILED",
1732
+ stage: "session_create",
1733
+ requestCategory: "session_create",
1734
+ statusClass: error instanceof Error && error.name === "AbortError" ? "timeout" : statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
1735
+ });
1736
+ }
1737
+ throw error;
1738
+ }
1739
+ }
1740
+ async createAndFetchSessionRequest(params, telemetryStartedAt) {
944
1741
  const wireProducts = params.products ?? foldIntoProducts(params.items, params.subscriptions);
945
1742
  const sessionCurrency = resolveSessionCurrency(
946
1743
  params.currency,
@@ -997,6 +1794,7 @@ var PaymentAPI = class {
997
1794
  headers[IDEMPOTENCY_KEY_HEADER] = idempotencyKey;
998
1795
  }
999
1796
  let response;
1797
+ let firstByteReported = false;
1000
1798
  for (let attempt = 0; ; attempt++) {
1001
1799
  response = await fetchWithNetworkRetry(
1002
1800
  `${this.baseUrl}/v1/checkouts/sessions?expand=true`,
@@ -1004,8 +1802,35 @@ var PaymentAPI = class {
1004
1802
  method: "POST",
1005
1803
  headers,
1006
1804
  body: JSON.stringify(payload)
1805
+ },
1806
+ NETWORK_RETRY_ATTEMPTS,
1807
+ (networkAttempt) => {
1808
+ this.telemetryHooks?.onRetry?.("session_create", networkAttempt);
1809
+ this.directTelemetry?.log({
1810
+ name: "operation.retry",
1811
+ stage: "session_create",
1812
+ requestCategory: "session_create",
1813
+ attempt: networkAttempt
1814
+ });
1007
1815
  }
1008
1816
  );
1817
+ if (!firstByteReported) {
1818
+ firstByteReported = true;
1819
+ const statusClass = `${Math.floor(response.status / 100)}xx`;
1820
+ this.directTelemetry?.log({
1821
+ name: "session.request.first_byte",
1822
+ stage: "session_first_byte",
1823
+ requestCategory: "session_create",
1824
+ statusClass
1825
+ });
1826
+ this.directTelemetry?.performance({
1827
+ stage: "session_first_byte",
1828
+ durationMs: this.telemetryTimestamp() - telemetryStartedAt,
1829
+ durationMode: "machine",
1830
+ requestCategory: "session_create",
1831
+ statusClass
1832
+ });
1833
+ }
1009
1834
  if (response.status === 204) {
1010
1835
  throw new FloPayError3(
1011
1836
  "Session auto-completed \u2014 payment method already on file",
@@ -1016,6 +1841,16 @@ var PaymentAPI = class {
1016
1841
  if (response.ok) break;
1017
1842
  const error = await buildApiErrorFromResponse(response, "Failed to create checkout session");
1018
1843
  if (error.code === IDEMPOTENCY_IN_PROGRESS_CODE && attempt < IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS) {
1844
+ try {
1845
+ this.telemetryHooks?.onRetry?.("session_create", attempt + 1);
1846
+ this.directTelemetry?.log({
1847
+ name: "operation.retry",
1848
+ stage: "session_create",
1849
+ requestCategory: "session_create",
1850
+ attempt: attempt + 1
1851
+ });
1852
+ } catch {
1853
+ }
1019
1854
  await delay(150 * 2 ** attempt);
1020
1855
  continue;
1021
1856
  }
@@ -1041,6 +1876,7 @@ var PaymentAPI = class {
1041
1876
  throw new FloPayError3("No session ID returned", "api_error");
1042
1877
  }
1043
1878
  this.autoCacheDisplayData(uuid, params);
1879
+ this.adoptDirectTelemetryCheckout(uuid);
1044
1880
  const unifiedSession = await this.getUnifiedCheckoutSession(uuid);
1045
1881
  return {
1046
1882
  ...unifiedSession,
@@ -1050,31 +1886,78 @@ var PaymentAPI = class {
1050
1886
  };
1051
1887
  }
1052
1888
  async waitForCheckoutSessionCompletion(checkoutSessionId, options) {
1889
+ this.beginDirectTelemetryCheckout(checkoutSessionId);
1890
+ const startedAt = this.telemetryTimestamp();
1891
+ this.directTelemetry?.log({
1892
+ name: "operation.recovery.started",
1893
+ stage: "recovery",
1894
+ requestCategory: "session_read",
1895
+ paymentMethodCategory: "saved"
1896
+ });
1053
1897
  const timeoutMs = options?.timeoutMs ?? DEFAULT_PROCESSING_TIMEOUT_MS;
1054
1898
  const deadline = Date.now() + timeoutMs;
1055
1899
  let nextDelayMs = this.clampRetryAfterMs(options?.initialDelayMs ?? DEFAULT_PROCESSING_RETRY_AFTER_MS);
1056
- while (true) {
1057
- const remainingMs = deadline - Date.now();
1058
- if (remainingMs <= 0) {
1059
- throw createCheckoutProcessingTimeoutError();
1060
- }
1061
- if (nextDelayMs > 0) {
1062
- await delay(Math.min(nextDelayMs, remainingMs));
1900
+ let pollAttempt = 0;
1901
+ try {
1902
+ while (true) {
1903
+ const remainingMs = deadline - Date.now();
1904
+ if (remainingMs <= 0) {
1905
+ throw createCheckoutProcessingTimeoutError();
1906
+ }
1907
+ if (nextDelayMs > 0) {
1908
+ try {
1909
+ pollAttempt += 1;
1910
+ this.telemetryHooks?.onRetry?.("session_read", pollAttempt);
1911
+ this.directTelemetry?.log({
1912
+ name: "operation.retry",
1913
+ stage: "recovery",
1914
+ requestCategory: "session_read",
1915
+ paymentMethodCategory: "saved",
1916
+ attempt: pollAttempt
1917
+ });
1918
+ } catch {
1919
+ }
1920
+ await delay(Math.min(nextDelayMs, remainingMs));
1921
+ if (Date.now() >= deadline) {
1922
+ throw createCheckoutProcessingTimeoutError();
1923
+ }
1924
+ }
1925
+ const session = await this.getUnifiedCheckoutSession(checkoutSessionId, options?.nonce);
1926
+ const status = session.data.session?.status;
1927
+ if (status === "complete" || status === "expired") {
1928
+ this.directTelemetry?.log({
1929
+ name: "operation.recovery.completed",
1930
+ stage: "recovery",
1931
+ requestCategory: "session_read",
1932
+ paymentMethodCategory: "saved"
1933
+ });
1934
+ this.directTelemetry?.performance({
1935
+ stage: "recovery",
1936
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1937
+ durationMode: "machine",
1938
+ requestCategory: "session_read",
1939
+ paymentMethodCategory: "saved"
1940
+ });
1941
+ return session;
1942
+ }
1063
1943
  if (Date.now() >= deadline) {
1064
1944
  throw createCheckoutProcessingTimeoutError();
1065
1945
  }
1946
+ nextDelayMs = this.clampRetryAfterMs(
1947
+ Math.max(nextDelayMs * 2, MIN_PROCESSING_RETRY_AFTER_MS)
1948
+ );
1066
1949
  }
1067
- const session = await this.getUnifiedCheckoutSession(checkoutSessionId, options?.nonce);
1068
- const status = session.data.session?.status;
1069
- if (status === "complete" || status === "expired") {
1070
- return session;
1071
- }
1072
- if (Date.now() >= deadline) {
1073
- throw createCheckoutProcessingTimeoutError();
1950
+ } catch (error) {
1951
+ if (error instanceof FloPayError3 && error.code === "checkout_processing_timeout") {
1952
+ this.reportDirectFailure(
1953
+ error,
1954
+ "RECOVERY_FAILED",
1955
+ "recovery",
1956
+ "session_read",
1957
+ "saved"
1958
+ );
1074
1959
  }
1075
- nextDelayMs = this.clampRetryAfterMs(
1076
- Math.max(nextDelayMs * 2, MIN_PROCESSING_RETRY_AFTER_MS)
1077
- );
1960
+ throw error;
1078
1961
  }
1079
1962
  }
1080
1963
  /** Normalize a raw session into a provider-agnostic shape. */
@@ -1295,13 +2178,86 @@ var PaymentAPI = class {
1295
2178
  };
1296
2179
  }
1297
2180
  };
2181
+ function createInstrumentedPaymentAPI(billingApiUrl, hooks) {
2182
+ const InstrumentedPaymentAPI = PaymentAPI;
2183
+ return new InstrumentedPaymentAPI(billingApiUrl, hooks);
2184
+ }
1298
2185
 
1299
2186
  // src/pci-vault-card-capture.ts
1300
- import { FloPayError as FloPayError4 } from "@flopay/shared";
2187
+ import {
2188
+ buildTelemetryErrorEvent as buildTelemetryErrorEvent2,
2189
+ buildTelemetryLogEvent as buildTelemetryLogEvent2,
2190
+ buildTelemetryTerminalEvent as buildTelemetryTerminalEvent2,
2191
+ FloPayError as FloPayError4,
2192
+ resolveBillingApiUrl,
2193
+ SDK_VERSION as SDK_VERSION2
2194
+ } from "@flopay/shared";
1301
2195
  var VAULT_MESSAGE_SOURCE = "flopay-vault";
1302
- function addBreadcrumb(message, data) {
1303
- const sentry = globalThis.Sentry;
1304
- sentry?.addBreadcrumb?.({ category: "flopay.card-capture", level: "info", message, data });
2196
+ var VAULT_NON_TERMINAL_OUTCOME_LOGS = {
2197
+ ready: ["vault.widget.ready", "vault_ready"],
2198
+ submitting: ["vault.submission.started", "vault_submit"],
2199
+ blocked: ["operation.state_transition", "vault_submit"],
2200
+ action_required: ["vault.action.required", "three_ds_handoff"]
2201
+ };
2202
+ function addBreadcrumb(event) {
2203
+ const data = {
2204
+ class: event.class,
2205
+ stage: event.stage
2206
+ };
2207
+ if ("code" in event) data["code"] = event.code;
2208
+ if ("provider" in event && event.provider) data["provider"] = event.provider;
2209
+ if ("paymentMethodCategory" in event && event.paymentMethodCategory) {
2210
+ data["paymentMethodCategory"] = event.paymentMethodCategory;
2211
+ }
2212
+ if ("outcome" in event) data["outcome"] = event.outcome;
2213
+ if ("durationMs" in event && event.durationMs !== void 0) data["durationMs"] = event.durationMs;
2214
+ if ("durationMode" in event && event.durationMode) data["durationMode"] = event.durationMode;
2215
+ try {
2216
+ const sentry = globalThis.Sentry;
2217
+ sentry?.addBreadcrumb?.({
2218
+ category: "flopay.telemetry",
2219
+ level: event.class === "technical_error" ? "error" : "info",
2220
+ message: event.class === "lifecycle" ? event.name : event.class === "technical_error" ? event.code : event.class === "expected_outcome" ? event.outcome : "sdk.performance",
2221
+ data
2222
+ });
2223
+ } catch {
2224
+ }
2225
+ }
2226
+ function vaultLog(name, stage) {
2227
+ return buildTelemetryLogEvent2({
2228
+ eventId: "11111111-1111-4111-8111-111111111111",
2229
+ name,
2230
+ stage,
2231
+ sequence: 0,
2232
+ provider: "pcivault",
2233
+ paymentMethodCategory: "card"
2234
+ });
2235
+ }
2236
+ function vaultErrorClassification(submissionStarted) {
2237
+ return submissionStarted ? { errorCode: "VAULT_SUBMIT_FAILED", stage: "vault_submit" } : { errorCode: "VAULT_LOAD_FAILED", stage: "vault_mount" };
2238
+ }
2239
+ function vaultOutcomeBreadcrumb(type, submissionStarted) {
2240
+ if (type === "complete" || type === "decline") {
2241
+ return buildTelemetryTerminalEvent2({
2242
+ eventId: "22222222-2222-4222-8222-222222222222",
2243
+ outcome: type === "complete" ? "payment_succeeded" : "payment_declined",
2244
+ sequence: 0,
2245
+ provider: "pcivault",
2246
+ paymentMethodCategory: "card"
2247
+ });
2248
+ }
2249
+ if (type === "error") {
2250
+ const classification = vaultErrorClassification(submissionStarted);
2251
+ return buildTelemetryErrorEvent2({
2252
+ eventId: "33333333-3333-4333-8333-333333333333",
2253
+ ...classification,
2254
+ sequence: 0,
2255
+ provider: "pcivault",
2256
+ paymentMethodCategory: "card"
2257
+ });
2258
+ }
2259
+ const [name, stage] = VAULT_NON_TERMINAL_OUTCOME_LOGS[type];
2260
+ return vaultLog(name, stage);
1305
2261
  }
1306
2262
  function isVaultResultMessage(value) {
1307
2263
  if (typeof value !== "object" || value === null) return false;
@@ -1319,7 +2275,7 @@ function isVaultResizeMessage(value) {
1319
2275
  return record["source"] === VAULT_MESSAGE_SOURCE && record["type"] === "resize" && typeof record["height"] === "number" && Number.isFinite(record["height"]);
1320
2276
  }
1321
2277
  var PciVaultCardCapture = class {
1322
- constructor(config = {}) {
2278
+ constructor(config = {}, internalTelemetry) {
1323
2279
  this.provider = "pcivault";
1324
2280
  this.container = null;
1325
2281
  this.messageHandler = null;
@@ -1351,11 +2307,33 @@ var PciVaultCardCapture = class {
1351
2307
  /** Latest card-field order + autofocus directive to push into the widget. */
1352
2308
  this.cardFieldOrder = null;
1353
2309
  this.cardAutoFocus = true;
2310
+ this.captureRequestedAt = 0;
2311
+ this.vaultReadyReported = false;
2312
+ this.submissionStarted = false;
2313
+ this.submissionStartedAt = null;
1354
2314
  this.listeners = /* @__PURE__ */ new Map();
1355
2315
  this.config = config;
2316
+ this.ownsTelemetryReporter = !internalTelemetry && config.telemetry !== false;
2317
+ this.telemetryReporter = internalTelemetry?.reporter ?? (this.ownsTelemetryReporter ? new TelemetryReporter({
2318
+ billingApiUrl: resolveBillingApiUrl(),
2319
+ sdkVersion: SDK_VERSION2
2320
+ }) : void 0);
2321
+ this.requestedAt = internalTelemetry?.requestedAt;
1356
2322
  }
1357
2323
  async mount(container, options) {
2324
+ if (this.ownsTelemetryReporter && !this.telemetryReporter) {
2325
+ this.telemetryReporter = new TelemetryReporter({
2326
+ billingApiUrl: resolveBillingApiUrl(),
2327
+ sdkVersion: SDK_VERSION2
2328
+ });
2329
+ }
2330
+ const mountedAt = this.telemetryReporter?.now?.() ?? telemetryNow();
2331
+ this.captureRequestedAt = this.requestedAt ?? mountedAt;
2332
+ this.vaultReadyReported = false;
2333
+ this.submissionStarted = false;
2334
+ this.submissionStartedAt = null;
1358
2335
  if (typeof window === "undefined" || typeof document === "undefined") {
2336
+ this.reportVaultLoadFailure();
1359
2337
  throw new FloPayError4(
1360
2338
  "The vault card form is only available in the browser.",
1361
2339
  "api_error",
@@ -1363,6 +2341,7 @@ var PciVaultCardCapture = class {
1363
2341
  );
1364
2342
  }
1365
2343
  if (!options?.html || !options.html.trim()) {
2344
+ this.reportVaultLoadFailure();
1366
2345
  throw new FloPayError4(
1367
2346
  "No vault capture widget HTML was provided to mount the secure card form.",
1368
2347
  "api_error",
@@ -1373,14 +2352,33 @@ var PciVaultCardCapture = class {
1373
2352
  this.messageToken = options.messageToken ?? null;
1374
2353
  this.expectedOrigin = options.expectedOrigin ?? this.config.expectedOrigin ?? null;
1375
2354
  this.theme = options.theme ?? null;
1376
- this.attachMessageListener();
1377
- this.injectWidget(container, options.html);
2355
+ try {
2356
+ this.attachMessageListener();
2357
+ this.injectWidget(container, options.html);
2358
+ } catch (error) {
2359
+ this.reportVaultLoadFailure();
2360
+ throw error;
2361
+ }
1378
2362
  this.postTheme();
1379
2363
  this.postSubmitGate();
1380
2364
  this.postCardFieldOrder();
1381
- addBreadcrumb("vault widget mounted", { sessionId: this.config.sessionId });
2365
+ addBreadcrumb(vaultLog("vault.widget.mounted", "vault_mount"));
2366
+ this.telemetryReporter?.log({
2367
+ name: "vault.widget.mounted",
2368
+ stage: "vault_mount",
2369
+ provider: "pcivault",
2370
+ paymentMethodCategory: "card"
2371
+ });
1382
2372
  this.emit("ready", { sessionId: this.config.sessionId });
1383
2373
  }
2374
+ reportVaultLoadFailure() {
2375
+ this.telemetryReporter?.error({
2376
+ errorCode: "VAULT_LOAD_FAILED",
2377
+ stage: "vault_mount",
2378
+ provider: "pcivault",
2379
+ paymentMethodCategory: "card"
2380
+ });
2381
+ }
1384
2382
  on(event, handler) {
1385
2383
  let set = this.listeners.get(event);
1386
2384
  if (!set) {
@@ -1404,6 +2402,10 @@ var PciVaultCardCapture = class {
1404
2402
  }
1405
2403
  this.messageToken = null;
1406
2404
  this.expectedOrigin = null;
2405
+ if (this.ownsTelemetryReporter) {
2406
+ this.telemetryReporter?.destroy();
2407
+ this.telemetryReporter = void 0;
2408
+ }
1407
2409
  }
1408
2410
  // ── internals ──
1409
2411
  /**
@@ -1457,10 +2459,12 @@ var PciVaultCardCapture = class {
1457
2459
  message: data.message,
1458
2460
  nextActionRedirectUrl: data.nextActionRedirectUrl
1459
2461
  };
1460
- addBreadcrumb(`vault widget ${data.type}`, {
1461
- sessionId: outcome.sessionId,
1462
- declineReason: outcome.declineReason
1463
- });
2462
+ if (data.type === "submitting") {
2463
+ this.submissionStarted = true;
2464
+ this.submissionStartedAt = this.telemetryReporter?.now?.() ?? telemetryNow();
2465
+ }
2466
+ addBreadcrumb(vaultOutcomeBreadcrumb(data.type, this.submissionStarted));
2467
+ this.reportOutcome(data.type);
1464
2468
  if (data.type === "ready") {
1465
2469
  this.postTheme();
1466
2470
  this.postSubmitGate();
@@ -1477,6 +2481,88 @@ var PciVaultCardCapture = class {
1477
2481
  this.messageHandler = handler;
1478
2482
  window.addEventListener("message", handler);
1479
2483
  }
2484
+ reportOutcome(type) {
2485
+ const reporter = this.telemetryReporter;
2486
+ if (!reporter) return;
2487
+ if (type === "ready" && !this.vaultReadyReported) {
2488
+ this.vaultReadyReported = true;
2489
+ reporter.performance({
2490
+ stage: "vault_ready",
2491
+ durationMs: Math.max(0, reporter.now() - this.captureRequestedAt),
2492
+ durationMode: "machine",
2493
+ provider: "pcivault",
2494
+ paymentMethodCategory: "card"
2495
+ });
2496
+ }
2497
+ if (type === "complete" || type === "decline") {
2498
+ reporter.log({
2499
+ name: "vault.terminal",
2500
+ stage: "completion",
2501
+ provider: "pcivault",
2502
+ paymentMethodCategory: "card"
2503
+ });
2504
+ reporter.terminal({
2505
+ outcome: type === "complete" ? "payment_succeeded" : "payment_declined",
2506
+ provider: "pcivault",
2507
+ paymentMethodCategory: "card"
2508
+ });
2509
+ return;
2510
+ }
2511
+ if (type === "error") {
2512
+ const classification = vaultErrorClassification(this.submissionStarted);
2513
+ reporter.log({
2514
+ name: "vault.terminal",
2515
+ stage: classification.stage,
2516
+ provider: "pcivault",
2517
+ paymentMethodCategory: "card"
2518
+ });
2519
+ reporter.error({
2520
+ ...classification,
2521
+ provider: "pcivault",
2522
+ paymentMethodCategory: "card"
2523
+ });
2524
+ return;
2525
+ }
2526
+ if (type === "action_required") {
2527
+ reporter.log({
2528
+ name: "vault.action.required",
2529
+ stage: "three_ds_handoff",
2530
+ provider: "pcivault",
2531
+ paymentMethodCategory: "card"
2532
+ });
2533
+ reporter.log({
2534
+ name: "vault.three_ds.handoff",
2535
+ stage: "three_ds_handoff",
2536
+ provider: "pcivault",
2537
+ paymentMethodCategory: "card"
2538
+ });
2539
+ const submissionStartedAt = this.submissionStartedAt;
2540
+ this.submissionStartedAt = null;
2541
+ if (submissionStartedAt !== null) {
2542
+ reporter.performance({
2543
+ stage: "three_ds_handoff",
2544
+ durationMs: Math.max(0, reporter.now() - submissionStartedAt),
2545
+ durationMode: "machine",
2546
+ provider: "pcivault",
2547
+ paymentMethodCategory: "card"
2548
+ });
2549
+ }
2550
+ reporter.terminal({
2551
+ outcome: "action_required",
2552
+ stage: "three_ds_handoff",
2553
+ provider: "pcivault",
2554
+ paymentMethodCategory: "card"
2555
+ });
2556
+ return;
2557
+ }
2558
+ const [name, stage] = VAULT_NON_TERMINAL_OUTCOME_LOGS[type];
2559
+ reporter.log({
2560
+ name,
2561
+ stage,
2562
+ provider: "pcivault",
2563
+ paymentMethodCategory: "card"
2564
+ });
2565
+ }
1480
2566
  /**
1481
2567
  * Push merchant theme colors into the hosted widget (live). The host calls
1482
2568
  * this on a runtime theme switch; the widget applies them to its CSS variables
@@ -1604,12 +2690,43 @@ var PciVaultCardCapture = class {
1604
2690
  ].join(";");
1605
2691
  frame.src = challengeUrl;
1606
2692
  backdrop.appendChild(frame);
2693
+ const closeButton = document.createElement("button");
2694
+ closeButton.type = "button";
2695
+ closeButton.setAttribute("aria-label", "Close card authentication");
2696
+ closeButton.textContent = "\xD7";
2697
+ closeButton.style.cssText = [
2698
+ "position:fixed",
2699
+ "top:20px",
2700
+ "right:20px",
2701
+ "width:40px",
2702
+ "height:40px",
2703
+ "border:0",
2704
+ "border-radius:9999px",
2705
+ "background:#fff",
2706
+ "color:#0f172a",
2707
+ "font-size:28px",
2708
+ "line-height:40px",
2709
+ "cursor:pointer",
2710
+ "box-shadow:0 4px 14px rgba(0,0,0,0.25)"
2711
+ ].join(";");
2712
+ closeButton.addEventListener("click", () => this.abandonActionRequiredOverlay());
2713
+ backdrop.appendChild(closeButton);
2714
+ backdrop.addEventListener("click", (event) => {
2715
+ if (event.target === backdrop) this.abandonActionRequiredOverlay();
2716
+ });
1607
2717
  const returnHandler = (event) => {
1608
2718
  if (event.source !== frame.contentWindow) return;
1609
2719
  const data = event.data;
1610
2720
  if (!data || typeof data !== "object") return;
1611
2721
  const record = data;
1612
2722
  if (record["source"] !== "flopay-vault-3ds-return") return;
2723
+ addBreadcrumb(vaultLog("vault.three_ds.returned", "three_ds_return"));
2724
+ this.telemetryReporter?.log({
2725
+ name: "vault.three_ds.returned",
2726
+ stage: "three_ds_return",
2727
+ provider: "pcivault",
2728
+ paymentMethodCategory: "card"
2729
+ });
1613
2730
  this.hideActionRequiredOverlay();
1614
2731
  this.postActionCompleted(record["status"]);
1615
2732
  };
@@ -1617,7 +2734,7 @@ var PciVaultCardCapture = class {
1617
2734
  this.threeDsReturnHandler = returnHandler;
1618
2735
  document.body.appendChild(backdrop);
1619
2736
  this.actionOverlay = backdrop;
1620
- addBreadcrumb("vault 3ds challenge overlay shown");
2737
+ addBreadcrumb(vaultLog("vault.three_ds.handoff", "three_ds_handoff"));
1621
2738
  }
1622
2739
  /**
1623
2740
  * Tell the vault widget that the buyer has completed (or abandoned) the
@@ -1643,6 +2760,26 @@ var PciVaultCardCapture = class {
1643
2760
  } catch {
1644
2761
  }
1645
2762
  }
2763
+ abandonActionRequiredOverlay() {
2764
+ if (!this.actionOverlay) return;
2765
+ const breadcrumb = buildTelemetryTerminalEvent2({
2766
+ eventId: "77777777-7777-4777-8777-777777777777",
2767
+ outcome: "customer_abandoned",
2768
+ stage: "three_ds_handoff",
2769
+ sequence: 0,
2770
+ provider: "pcivault",
2771
+ paymentMethodCategory: "card"
2772
+ });
2773
+ addBreadcrumb(breadcrumb);
2774
+ this.telemetryReporter?.terminal({
2775
+ outcome: "customer_abandoned",
2776
+ stage: "three_ds_handoff",
2777
+ provider: "pcivault",
2778
+ paymentMethodCategory: "card"
2779
+ });
2780
+ this.hideActionRequiredOverlay();
2781
+ this.postActionCompleted("abandoned");
2782
+ }
1646
2783
  hideActionRequiredOverlay() {
1647
2784
  if (this.threeDsReturnHandler) {
1648
2785
  window.removeEventListener("message", this.threeDsReturnHandler);
@@ -1651,7 +2788,6 @@ var PciVaultCardCapture = class {
1651
2788
  if (!this.actionOverlay) return;
1652
2789
  this.actionOverlay.parentNode?.removeChild(this.actionOverlay);
1653
2790
  this.actionOverlay = null;
1654
- addBreadcrumb("vault 3ds challenge overlay hidden");
1655
2791
  }
1656
2792
  /**
1657
2793
  * Size the hosted-widget iframe to the height reported by the form inside it.
@@ -1667,13 +2803,62 @@ var PciVaultCardCapture = class {
1667
2803
  iframe.style.height = `${clamped}px`;
1668
2804
  }
1669
2805
  };
2806
+ function createInstrumentedPciVaultCardCapture(config, reporter, requestedAt) {
2807
+ const InstrumentedCapture = PciVaultCardCapture;
2808
+ return new InstrumentedCapture(config, { reporter, requestedAt });
2809
+ }
2810
+
2811
+ // src/telemetry-bridge.ts
2812
+ var FLOPAY_TELEMETRY_BRIDGE = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.bridge.v1");
2813
+ function attachFloPayTelemetryBridge(target, reporter, fallbackNow) {
2814
+ const now = () => reporter?.now() ?? fallbackNow();
2815
+ const bridge = {
2816
+ error: (input) => reporter?.error(input),
2817
+ log: (input) => reporter?.log(input),
2818
+ performance: (input) => reporter?.performance(input),
2819
+ terminal: (input) => reporter?.terminal(input),
2820
+ now,
2821
+ elapsed: (startedAt) => Math.max(0, now() - startedAt),
2822
+ setCheckoutContext: (context) => reporter?.setCheckoutContext(context),
2823
+ beginCheckout: (context = {}) => reporter?.beginCheckout(context) ?? now(),
2824
+ disable: () => reporter?.disable()
2825
+ };
2826
+ Object.defineProperty(target, FLOPAY_TELEMETRY_BRIDGE, {
2827
+ configurable: false,
2828
+ enumerable: false,
2829
+ writable: false,
2830
+ value: bridge
2831
+ });
2832
+ }
2833
+ function getFloPayTelemetryBridge(target) {
2834
+ return target[FLOPAY_TELEMETRY_BRIDGE];
2835
+ }
1670
2836
 
1671
2837
  // src/flopay.ts
2838
+ function isExpectedDecline(error) {
2839
+ if (!error) return false;
2840
+ const code = error.code?.toLowerCase() ?? "";
2841
+ return Boolean(error.declineCode) || code.includes("declin");
2842
+ }
2843
+ function telemetryProvider(name) {
2844
+ if (name === "stripe" || name === "paypal" || name === "pcivault") return name;
2845
+ return "other";
2846
+ }
1672
2847
  var FloPay = class {
1673
- constructor(provider, config) {
2848
+ constructor(provider, config, telemetryReporter) {
1674
2849
  this.currentElements = null;
2850
+ this.cardThreeDsStartedAt = /* @__PURE__ */ new Map();
1675
2851
  this.provider = provider;
1676
2852
  this.config = config;
2853
+ this.telemetryReporter = telemetryReporter ?? new TelemetryReporter({
2854
+ billingApiUrl: resolveBillingApiUrl2(config.billingApiUrl),
2855
+ sdkVersion: SDK_VERSION3,
2856
+ enabled: config.telemetry !== false
2857
+ });
2858
+ attachFloPayTelemetryBridge(this, this.telemetryReporter, telemetryNow);
2859
+ }
2860
+ now() {
2861
+ return this.telemetryReporter?.now?.() ?? telemetryNow();
1677
2862
  }
1678
2863
  /**
1679
2864
  * Creates a new `FloPayElements` group for mounting payment fields.
@@ -1697,11 +2882,169 @@ var FloPay = class {
1697
2882
  }
1698
2883
  /** Create a payment method from the current elements (tokenize card). */
1699
2884
  async createPaymentMethod(billingDetails) {
1700
- return this.provider.createPaymentMethod(billingDetails);
2885
+ const started = this.now();
2886
+ const provider = telemetryProvider(this.provider.name);
2887
+ this.telemetryReporter?.log({
2888
+ name: "payment.tokenization.started",
2889
+ stage: "tokenization",
2890
+ provider,
2891
+ paymentMethodCategory: "card"
2892
+ });
2893
+ try {
2894
+ const result = await this.provider.createPaymentMethod(billingDetails);
2895
+ this.telemetryReporter?.performance({
2896
+ stage: "tokenization",
2897
+ durationMs: this.now() - started,
2898
+ durationMode: "machine",
2899
+ provider,
2900
+ paymentMethodCategory: "card"
2901
+ });
2902
+ if (!result.error) {
2903
+ this.telemetryReporter?.log({
2904
+ name: "payment.tokenization.completed",
2905
+ stage: "tokenization",
2906
+ provider,
2907
+ paymentMethodCategory: "card"
2908
+ });
2909
+ } else if (result.error.type !== "validation_error") {
2910
+ this.telemetryReporter?.error({
2911
+ errorCode: "TOKENIZATION_FAILED",
2912
+ stage: "tokenization",
2913
+ provider,
2914
+ paymentMethodCategory: "card"
2915
+ });
2916
+ }
2917
+ return result;
2918
+ } catch (error) {
2919
+ this.telemetryReporter?.performance({
2920
+ stage: "tokenization",
2921
+ durationMs: this.now() - started,
2922
+ durationMode: "machine",
2923
+ provider,
2924
+ paymentMethodCategory: "card"
2925
+ });
2926
+ this.telemetryReporter?.error({
2927
+ errorCode: "TOKENIZATION_FAILED",
2928
+ stage: "tokenization",
2929
+ provider,
2930
+ paymentMethodCategory: "card"
2931
+ });
2932
+ throw error;
2933
+ }
1701
2934
  }
1702
2935
  /** Confirm a card payment with a known client secret and payment method ID. */
1703
2936
  async confirmCardPayment(params) {
1704
- return this.provider.confirmCardPayment(params);
2937
+ const provider = telemetryProvider(this.provider.name);
2938
+ const operationStartedAt = this.now();
2939
+ try {
2940
+ const result = await this.provider.confirmCardPayment(params);
2941
+ const completedAt = this.now();
2942
+ const threeDs = result.threeDs;
2943
+ let threeDsFailed = false;
2944
+ if (threeDs?.status === "handoff") {
2945
+ if (this.trackCardThreeDsAttempt(threeDs.attemptId, completedAt)) {
2946
+ this.telemetryReporter?.log({
2947
+ name: "payment.three_ds.handoff",
2948
+ stage: "three_ds_handoff",
2949
+ provider,
2950
+ paymentMethodCategory: "card"
2951
+ });
2952
+ this.telemetryReporter?.performance({
2953
+ stage: "three_ds_handoff",
2954
+ durationMs: completedAt - operationStartedAt,
2955
+ durationMode: "machine",
2956
+ provider,
2957
+ paymentMethodCategory: "card"
2958
+ });
2959
+ this.telemetryReporter?.terminal({
2960
+ outcome: "action_required",
2961
+ stage: "three_ds_handoff",
2962
+ provider,
2963
+ paymentMethodCategory: "card"
2964
+ });
2965
+ }
2966
+ } else if (threeDs) {
2967
+ const threeDsStartedAt = this.takeCardThreeDsAttempt(threeDs.attemptId, completedAt);
2968
+ if (threeDsStartedAt !== void 0) {
2969
+ if (threeDs.status === "returned") {
2970
+ this.telemetryReporter?.log({
2971
+ name: "payment.three_ds.returned",
2972
+ stage: "three_ds_return",
2973
+ provider,
2974
+ paymentMethodCategory: "card"
2975
+ });
2976
+ this.telemetryReporter?.performance({
2977
+ stage: "three_ds_return",
2978
+ durationMs: completedAt - threeDsStartedAt,
2979
+ durationMode: "machine",
2980
+ provider,
2981
+ paymentMethodCategory: "card"
2982
+ });
2983
+ } else {
2984
+ threeDsFailed = true;
2985
+ }
2986
+ }
2987
+ }
2988
+ if (isExpectedDecline(result.error)) {
2989
+ this.telemetryReporter?.terminal({
2990
+ outcome: "payment_declined",
2991
+ provider,
2992
+ paymentMethodCategory: "card"
2993
+ });
2994
+ } else if (result.error?.type === "validation_error") {
2995
+ this.telemetryReporter?.terminal({
2996
+ outcome: "validation_rejected",
2997
+ provider,
2998
+ paymentMethodCategory: "card"
2999
+ });
3000
+ } else if (result.error) {
3001
+ this.telemetryReporter?.error({
3002
+ errorCode: threeDsFailed ? "THREE_DS_FAILED" : "PROVIDER_RUNTIME_FAILED",
3003
+ stage: threeDsFailed ? "three_ds_return" : "processing",
3004
+ provider,
3005
+ paymentMethodCategory: "card"
3006
+ });
3007
+ } else if (!threeDsFailed && threeDs?.status !== "handoff" && result.status === "succeeded") {
3008
+ this.telemetryReporter?.terminal({
3009
+ outcome: "payment_succeeded",
3010
+ provider,
3011
+ paymentMethodCategory: "card"
3012
+ });
3013
+ }
3014
+ return result;
3015
+ } catch (error) {
3016
+ this.telemetryReporter?.error({
3017
+ errorCode: "PROVIDER_RUNTIME_FAILED",
3018
+ stage: "processing",
3019
+ provider,
3020
+ paymentMethodCategory: "card"
3021
+ });
3022
+ throw error;
3023
+ }
3024
+ }
3025
+ trackCardThreeDsAttempt(attemptId, startedAt) {
3026
+ this.pruneExpiredCardThreeDsAttempts(startedAt);
3027
+ if (this.cardThreeDsStartedAt.has(attemptId)) return false;
3028
+ while (this.cardThreeDsStartedAt.size >= MAX_CARD_THREE_DS_ATTEMPTS) {
3029
+ const oldestAttemptId = this.cardThreeDsStartedAt.keys().next().value;
3030
+ if (oldestAttemptId === void 0) break;
3031
+ this.cardThreeDsStartedAt.delete(oldestAttemptId);
3032
+ }
3033
+ this.cardThreeDsStartedAt.set(attemptId, startedAt);
3034
+ return true;
3035
+ }
3036
+ takeCardThreeDsAttempt(attemptId, now) {
3037
+ this.pruneExpiredCardThreeDsAttempts(now);
3038
+ const startedAt = this.cardThreeDsStartedAt.get(attemptId);
3039
+ if (startedAt !== void 0) this.cardThreeDsStartedAt.delete(attemptId);
3040
+ return startedAt;
3041
+ }
3042
+ pruneExpiredCardThreeDsAttempts(now) {
3043
+ for (const [attemptId, startedAt] of this.cardThreeDsStartedAt) {
3044
+ if (now - startedAt >= CARD_THREE_DS_ATTEMPT_TTL_MS) {
3045
+ this.cardThreeDsStartedAt.delete(attemptId);
3046
+ }
3047
+ }
1705
3048
  }
1706
3049
  /**
1707
3050
  * Create a {@link CardCaptureAdapter} for collecting card details through the
@@ -1716,21 +3059,270 @@ var FloPay = class {
1716
3059
  * the SDK runtime.
1717
3060
  */
1718
3061
  cardCapture(options) {
3062
+ const requestedAt = this.now();
3063
+ this.telemetryReporter?.log({
3064
+ name: "vault.capture.requested",
3065
+ stage: "vault_request",
3066
+ provider: "pcivault",
3067
+ paymentMethodCategory: "card"
3068
+ });
3069
+ if (this.telemetryReporter) {
3070
+ return createInstrumentedPciVaultCardCapture(
3071
+ { sessionId: options?.sessionId },
3072
+ this.telemetryReporter,
3073
+ requestedAt
3074
+ );
3075
+ }
1719
3076
  return new PciVaultCardCapture({
1720
- sessionId: options?.sessionId
3077
+ sessionId: options?.sessionId,
3078
+ telemetry: false
1721
3079
  });
1722
3080
  }
1723
3081
  /** Confirm a PayPal payment: create intent via billing API → confirm → redirect if needed. */
1724
3082
  async confirmPayPalPayment(params) {
1725
- return this.provider.confirmPayPalPayment(params);
3083
+ const startedAt = this.now();
3084
+ this.telemetryReporter?.log({
3085
+ name: "payment.method.selected",
3086
+ stage: "processing",
3087
+ provider: "paypal",
3088
+ paymentMethodCategory: "paypal"
3089
+ });
3090
+ this.telemetryReporter?.log({
3091
+ name: "payment.intent.started",
3092
+ stage: "processing",
3093
+ provider: "paypal",
3094
+ paymentMethodCategory: "paypal",
3095
+ requestCategory: "intent_create"
3096
+ });
3097
+ try {
3098
+ const result = await this.provider.confirmPayPalPayment(params);
3099
+ this.telemetryReporter?.performance({
3100
+ stage: "processing",
3101
+ durationMs: this.now() - startedAt,
3102
+ durationMode: "machine",
3103
+ provider: "paypal",
3104
+ paymentMethodCategory: "paypal"
3105
+ });
3106
+ if (!result.error) {
3107
+ this.telemetryReporter?.log({
3108
+ name: "payment.intent.completed",
3109
+ stage: "processing",
3110
+ provider: "paypal",
3111
+ paymentMethodCategory: "paypal",
3112
+ requestCategory: "intent_create",
3113
+ statusClass: "2xx"
3114
+ });
3115
+ }
3116
+ if (result.status === "requires_action") {
3117
+ this.telemetryReporter?.log({
3118
+ name: "provider.redirect.started",
3119
+ stage: "redirect",
3120
+ provider: "paypal",
3121
+ paymentMethodCategory: "paypal"
3122
+ });
3123
+ this.telemetryReporter?.terminal({
3124
+ outcome: "action_required",
3125
+ stage: "redirect",
3126
+ provider: "paypal",
3127
+ paymentMethodCategory: "paypal"
3128
+ });
3129
+ } else if (result.status === "succeeded") {
3130
+ this.telemetryReporter?.terminal({
3131
+ outcome: "payment_succeeded",
3132
+ provider: "paypal",
3133
+ paymentMethodCategory: "paypal"
3134
+ });
3135
+ } else if (result.status !== "processing") {
3136
+ if (isExpectedDecline(result.error)) {
3137
+ this.telemetryReporter?.terminal({
3138
+ outcome: "payment_declined",
3139
+ provider: "paypal",
3140
+ paymentMethodCategory: "paypal"
3141
+ });
3142
+ } else if (result.error?.type === "validation_error") {
3143
+ this.telemetryReporter?.terminal({
3144
+ outcome: "validation_rejected",
3145
+ provider: "paypal",
3146
+ paymentMethodCategory: "paypal"
3147
+ });
3148
+ } else if (result.error) {
3149
+ this.telemetryReporter?.error({
3150
+ errorCode: "PAYMENT_PROCESSING_FAILED",
3151
+ stage: "processing",
3152
+ provider: "paypal",
3153
+ paymentMethodCategory: "paypal",
3154
+ requestCategory: "intent_create"
3155
+ });
3156
+ }
3157
+ }
3158
+ return result;
3159
+ } catch (error) {
3160
+ this.telemetryReporter?.performance({
3161
+ stage: "processing",
3162
+ durationMs: this.now() - startedAt,
3163
+ durationMode: "machine",
3164
+ provider: "paypal",
3165
+ paymentMethodCategory: "paypal"
3166
+ });
3167
+ this.telemetryReporter?.error({
3168
+ errorCode: "NETWORK_REQUEST_FAILED",
3169
+ stage: "processing",
3170
+ provider: "paypal",
3171
+ paymentMethodCategory: "paypal",
3172
+ requestCategory: "intent_create",
3173
+ statusClass: "network_error"
3174
+ });
3175
+ throw error;
3176
+ }
1726
3177
  }
1727
3178
  /** Resume a PayPal payment after redirect return. Returns null if no PayPal params in URL. */
1728
3179
  async resumePayPalPayment() {
1729
- return this.provider.resumePayPalPayment();
3180
+ const startedAt = this.now();
3181
+ try {
3182
+ const result = await this.provider.resumePayPalPayment();
3183
+ if (result === null) return null;
3184
+ this.telemetryReporter?.log({
3185
+ name: "provider.redirect.resumed",
3186
+ stage: "redirect_resume",
3187
+ provider: "paypal",
3188
+ paymentMethodCategory: "paypal"
3189
+ });
3190
+ this.telemetryReporter?.performance({
3191
+ stage: "redirect_resume",
3192
+ durationMs: this.now() - startedAt,
3193
+ durationMode: "machine",
3194
+ provider: "paypal",
3195
+ paymentMethodCategory: "paypal"
3196
+ });
3197
+ if (result.status === "succeeded") {
3198
+ this.telemetryReporter?.terminal({
3199
+ outcome: "payment_succeeded",
3200
+ provider: "paypal",
3201
+ paymentMethodCategory: "paypal"
3202
+ });
3203
+ } else if (isExpectedDecline(result.error)) {
3204
+ this.telemetryReporter?.terminal({
3205
+ outcome: "payment_declined",
3206
+ provider: "paypal",
3207
+ paymentMethodCategory: "paypal"
3208
+ });
3209
+ } else if (result.error?.type === "validation_error") {
3210
+ this.telemetryReporter?.terminal({
3211
+ outcome: "validation_rejected",
3212
+ provider: "paypal",
3213
+ paymentMethodCategory: "paypal"
3214
+ });
3215
+ } else if (result.error) {
3216
+ this.telemetryReporter?.error({
3217
+ errorCode: "REDIRECT_RESUME_FAILED",
3218
+ stage: "redirect_resume",
3219
+ provider: "paypal",
3220
+ paymentMethodCategory: "paypal"
3221
+ });
3222
+ }
3223
+ return result;
3224
+ } catch (error) {
3225
+ this.telemetryReporter?.performance({
3226
+ stage: "redirect_resume",
3227
+ durationMs: this.now() - startedAt,
3228
+ durationMode: "machine",
3229
+ provider: "paypal",
3230
+ paymentMethodCategory: "paypal"
3231
+ });
3232
+ this.telemetryReporter?.error({
3233
+ errorCode: "REDIRECT_RESUME_FAILED",
3234
+ stage: "redirect_resume",
3235
+ provider: "paypal",
3236
+ paymentMethodCategory: "paypal"
3237
+ });
3238
+ throw error;
3239
+ }
1730
3240
  }
1731
3241
  /** Confirms a payment using the mounted elements. */
1732
3242
  async confirmPayment(params) {
1733
- return this.provider.confirmPayment(params);
3243
+ const started = this.now();
3244
+ const provider = telemetryProvider(this.provider.name);
3245
+ this.telemetryReporter?.log({
3246
+ name: "payment.processing.started",
3247
+ stage: "processing",
3248
+ provider,
3249
+ paymentMethodCategory: "unknown"
3250
+ });
3251
+ try {
3252
+ const result = await this.provider.confirmPayment(params);
3253
+ const durationMs = this.now() - started;
3254
+ this.telemetryReporter?.performance({
3255
+ stage: "processing",
3256
+ durationMs,
3257
+ durationMode: "machine",
3258
+ provider,
3259
+ paymentMethodCategory: "unknown"
3260
+ });
3261
+ if (result.status === "succeeded") {
3262
+ this.telemetryReporter?.terminal({
3263
+ outcome: "payment_succeeded",
3264
+ provider,
3265
+ paymentMethodCategory: "unknown"
3266
+ });
3267
+ } else if (isExpectedDecline(result.error)) {
3268
+ this.telemetryReporter?.terminal({
3269
+ outcome: "payment_declined",
3270
+ provider,
3271
+ paymentMethodCategory: "unknown"
3272
+ });
3273
+ } else if (result.error?.type === "validation_error") {
3274
+ this.telemetryReporter?.terminal({
3275
+ outcome: "validation_rejected",
3276
+ provider,
3277
+ paymentMethodCategory: "unknown"
3278
+ });
3279
+ } else if (result.status === "requires_action") {
3280
+ this.telemetryReporter?.terminal({
3281
+ outcome: "action_required",
3282
+ stage: "three_ds_handoff",
3283
+ provider,
3284
+ paymentMethodCategory: "unknown"
3285
+ });
3286
+ } else if (result.status === "failed" && result.error) {
3287
+ this.telemetryReporter?.error({
3288
+ errorCode: "PAYMENT_PROCESSING_FAILED",
3289
+ stage: "processing",
3290
+ provider,
3291
+ paymentMethodCategory: "unknown"
3292
+ });
3293
+ }
3294
+ if (result.status === "succeeded" || result.status === "failed") {
3295
+ this.telemetryReporter?.log({
3296
+ name: "payment.processing.completed",
3297
+ stage: "processing",
3298
+ provider,
3299
+ paymentMethodCategory: "unknown"
3300
+ });
3301
+ } else {
3302
+ this.telemetryReporter?.log({
3303
+ name: "operation.state_transition",
3304
+ stage: result.status === "requires_action" ? "three_ds_handoff" : "processing",
3305
+ provider,
3306
+ paymentMethodCategory: "unknown"
3307
+ });
3308
+ }
3309
+ return result;
3310
+ } catch (error) {
3311
+ this.telemetryReporter?.performance({
3312
+ stage: "processing",
3313
+ durationMs: this.now() - started,
3314
+ durationMode: "machine",
3315
+ provider,
3316
+ paymentMethodCategory: "unknown"
3317
+ });
3318
+ this.telemetryReporter?.error({
3319
+ errorCode: "PAYMENT_PROCESSING_FAILED",
3320
+ stage: "processing",
3321
+ provider,
3322
+ paymentMethodCategory: "unknown"
3323
+ });
3324
+ throw error;
3325
+ }
1734
3326
  }
1735
3327
  /**
1736
3328
  * Retrieves a checkout session by ID via the billing API.
@@ -1749,9 +3341,7 @@ var FloPay = class {
1749
3341
  { param: "sessionId" }
1750
3342
  );
1751
3343
  }
1752
- const apiUrl = resolveBillingApiUrl(billingApiUrl ?? this.config.billingApiUrl);
1753
- const api = new PaymentAPI(apiUrl);
1754
- const unified = await api.getUnifiedCheckoutSession(sessionId);
3344
+ const unified = await this.retrieveUnifiedSession(sessionId, billingApiUrl);
1755
3345
  if (!unified.data.session) {
1756
3346
  throw new FloPayError5("Session not found", "api_error");
1757
3347
  }
@@ -1772,9 +3362,80 @@ var FloPay = class {
1772
3362
  { param: "sessionId" }
1773
3363
  );
1774
3364
  }
1775
- const apiUrl = resolveBillingApiUrl(billingApiUrl ?? this.config.billingApiUrl);
1776
- const api = new PaymentAPI(apiUrl);
1777
- return api.getUnifiedCheckoutSession(sessionId);
3365
+ const apiUrl = resolveBillingApiUrl2(billingApiUrl ?? this.config.billingApiUrl);
3366
+ const started = this.now();
3367
+ this.telemetryReporter?.log({
3368
+ name: "session.read.started",
3369
+ stage: "session_read",
3370
+ requestCategory: "session_read"
3371
+ });
3372
+ let firstByteDuration;
3373
+ const api = createInstrumentedPaymentAPI(apiUrl, {
3374
+ now: () => this.telemetryReporter?.now() ?? telemetryNow(),
3375
+ onFirstByte: (durationMs) => {
3376
+ firstByteDuration = durationMs;
3377
+ },
3378
+ onRetry: (requestCategory, attempt) => {
3379
+ this.telemetryReporter?.log({
3380
+ name: "operation.retry",
3381
+ stage: requestCategory === "session_read" ? "session_read" : "processing",
3382
+ requestCategory,
3383
+ attempt
3384
+ });
3385
+ }
3386
+ });
3387
+ try {
3388
+ const result = await api.getUnifiedCheckoutSession(sessionId);
3389
+ if (firstByteDuration !== void 0) {
3390
+ this.telemetryReporter?.log({
3391
+ name: "session.request.first_byte",
3392
+ stage: "session_first_byte",
3393
+ requestCategory: "session_read",
3394
+ statusClass: "2xx"
3395
+ });
3396
+ this.telemetryReporter?.performance({
3397
+ stage: "session_first_byte",
3398
+ durationMs: firstByteDuration,
3399
+ durationMode: "machine",
3400
+ requestCategory: "session_read",
3401
+ statusClass: "2xx"
3402
+ });
3403
+ }
3404
+ this.telemetryReporter?.log({
3405
+ name: "session.request.completed",
3406
+ stage: "session_complete",
3407
+ requestCategory: "session_read",
3408
+ statusClass: "2xx"
3409
+ });
3410
+ this.telemetryReporter?.log({
3411
+ name: "checkout.data.ready",
3412
+ stage: "checkout_data_ready"
3413
+ });
3414
+ this.telemetryReporter?.performance({
3415
+ stage: "session_complete",
3416
+ durationMs: this.now() - started,
3417
+ durationMode: "machine",
3418
+ requestCategory: "session_read",
3419
+ statusClass: "2xx"
3420
+ });
3421
+ return result;
3422
+ } catch (error) {
3423
+ const statusCode = error instanceof FloPayError5 ? error.statusCode : void 0;
3424
+ this.telemetryReporter?.error({
3425
+ errorCode: error instanceof FloPayError5 && error.code === "checkout_processing_timeout" ? "REQUEST_TIMEOUT" : "NETWORK_REQUEST_FAILED",
3426
+ stage: "session_read",
3427
+ provider: "flo",
3428
+ paymentMethodCategory: "unknown"
3429
+ });
3430
+ this.telemetryReporter?.performance({
3431
+ stage: "session_complete",
3432
+ durationMs: this.now() - started,
3433
+ durationMode: "machine",
3434
+ requestCategory: "session_read",
3435
+ statusClass: statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
3436
+ });
3437
+ throw error;
3438
+ }
1778
3439
  }
1779
3440
  /**
1780
3441
  * Returns the raw underlying provider instance (e.g. Stripe object).
@@ -1786,33 +3447,137 @@ var FloPay = class {
1786
3447
  }
1787
3448
  /** Tears down the SDK instance and releases resources. */
1788
3449
  destroy() {
3450
+ this.telemetryReporter?.log({ name: "checkout.unmount", stage: "unmount" });
3451
+ this.telemetryReporter?.destroy();
3452
+ this.cardThreeDsStartedAt.clear();
1789
3453
  this.currentElements?.destroy();
1790
3454
  this.currentElements = null;
1791
3455
  this.provider.destroy();
1792
3456
  }
1793
3457
  };
3458
+ function createInstrumentedFloPay(provider, config, reporter) {
3459
+ const InstrumentedFloPay = FloPay;
3460
+ return new InstrumentedFloPay(provider, config, reporter);
3461
+ }
1794
3462
 
1795
3463
  // src/load.ts
1796
3464
  var instanceCache = /* @__PURE__ */ new Map();
3465
+ function stableCacheValue(value) {
3466
+ if (Array.isArray(value)) return value.map(stableCacheValue);
3467
+ if (value && typeof value === "object") {
3468
+ return Object.fromEntries(
3469
+ Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, stableCacheValue(entry)])
3470
+ );
3471
+ }
3472
+ return value;
3473
+ }
3474
+ function instanceCacheKey(publishableKey, options) {
3475
+ return JSON.stringify([
3476
+ publishableKey,
3477
+ resolveBillingApiUrl3(options?.billingApiUrl),
3478
+ options?.telemetry !== false,
3479
+ options?.locale ?? "auto",
3480
+ options?.apiVersion ?? null,
3481
+ stableCacheValue(options?.appearance ?? null)
3482
+ ]);
3483
+ }
3484
+ var initializationCache = /* @__PURE__ */ new Map();
1797
3485
  async function loadFloPay(publishableKey, options) {
1798
3486
  if (!publishableKey) {
3487
+ const reporter = new TelemetryReporter({
3488
+ billingApiUrl: resolveBillingApiUrl3(options?.billingApiUrl),
3489
+ sdkVersion: SDK_VERSION4,
3490
+ enabled: options?.telemetry !== false
3491
+ });
3492
+ reporter.error({
3493
+ errorCode: "CONFIGURATION_INVALID",
3494
+ stage: "sdk_initialize",
3495
+ paymentMethodCategory: "unknown"
3496
+ });
3497
+ void reporter.flush().catch(() => {
3498
+ }).finally(() => reporter.destroy());
1799
3499
  throw new FloPayError6(
1800
3500
  "A publishable key is required to initialize FloPay.",
1801
3501
  "validation_error",
1802
3502
  { param: "publishableKey" }
1803
3503
  );
1804
3504
  }
1805
- const cached = instanceCache.get(publishableKey);
1806
- if (cached) return cached;
3505
+ const cacheKey = instanceCacheKey(publishableKey, options);
3506
+ const cached = instanceCache.get(cacheKey);
3507
+ if (cached) {
3508
+ if (options?.telemetry !== false) {
3509
+ getFloPayTelemetryBridge(cached)?.log({
3510
+ name: "sdk.cache.hit",
3511
+ stage: "sdk_initialize"
3512
+ });
3513
+ }
3514
+ return cached;
3515
+ }
3516
+ const initializing = initializationCache.get(cacheKey);
3517
+ if (initializing) return initializing;
1807
3518
  const config = {
1808
- publishableKey,
1809
- ...options
3519
+ ...options,
3520
+ publishableKey
1810
3521
  };
1811
- const adapter = new StripeAdapter();
1812
- await adapter.initialize(config);
1813
- const instance = new FloPay(adapter, config);
1814
- instanceCache.set(publishableKey, instance);
1815
- return instance;
3522
+ const initialization = (async () => {
3523
+ const reporter = new TelemetryReporter({
3524
+ billingApiUrl: resolveBillingApiUrl3(config.billingApiUrl),
3525
+ sdkVersion: SDK_VERSION4,
3526
+ enabled: config.telemetry !== false
3527
+ });
3528
+ const initializationStarted = reporter.now();
3529
+ reporter.log({ name: "sdk.initialize.started", stage: "sdk_initialize" });
3530
+ reporter.log({ name: "sdk.cache.miss", stage: "sdk_initialize" });
3531
+ reporter.log({
3532
+ name: "provider.load.started",
3533
+ stage: "provider_load",
3534
+ provider: "stripe"
3535
+ });
3536
+ const adapter = new StripeAdapter();
3537
+ try {
3538
+ await adapter.initialize(config);
3539
+ } catch (error) {
3540
+ reporter.error({
3541
+ errorCode: "SDK_INITIALIZATION_FAILED",
3542
+ stage: "sdk_initialize",
3543
+ provider: "stripe",
3544
+ paymentMethodCategory: "unknown"
3545
+ });
3546
+ reporter.destroy();
3547
+ throw error;
3548
+ }
3549
+ reporter.log({ name: "provider.ready", stage: "provider_ready", provider: "stripe" });
3550
+ reporter.log({
3551
+ name: "provider.availability.checked",
3552
+ stage: "provider_ready",
3553
+ provider: "stripe"
3554
+ });
3555
+ reporter.log({ name: "sdk.initialize.ready", stage: "sdk_initialize" });
3556
+ const initializationDuration = reporter.now() - initializationStarted;
3557
+ reporter.performance({
3558
+ stage: "sdk_initialize",
3559
+ durationMs: initializationDuration,
3560
+ durationMode: "machine",
3561
+ provider: "stripe"
3562
+ });
3563
+ reporter.performance({
3564
+ stage: "provider_ready",
3565
+ durationMs: initializationDuration,
3566
+ durationMode: "machine",
3567
+ provider: "stripe"
3568
+ });
3569
+ const instance = createInstrumentedFloPay(adapter, config, reporter);
3570
+ instanceCache.set(cacheKey, instance);
3571
+ return instance;
3572
+ })();
3573
+ initializationCache.set(cacheKey, initialization);
3574
+ try {
3575
+ return await initialization;
3576
+ } finally {
3577
+ if (initializationCache.get(cacheKey) === initialization) {
3578
+ initializationCache.delete(cacheKey);
3579
+ }
3580
+ }
1816
3581
  }
1817
3582
 
1818
3583
  // src/create-checkout-session.ts
@@ -1820,20 +3585,17 @@ import {
1820
3585
  FloPayError as FloPayError7,
1821
3586
  IDEMPOTENCY_IN_PROGRESS_CODE as IDEMPOTENCY_IN_PROGRESS_CODE2,
1822
3587
  IDEMPOTENCY_KEY_HEADER as IDEMPOTENCY_KEY_HEADER2,
1823
- SDK_VERSION as SDK_VERSION2,
3588
+ SDK_VERSION as SDK_VERSION5,
1824
3589
  buildProductPayload as buildProductPayload2,
1825
3590
  foldIntoProducts as foldIntoProducts2,
1826
3591
  resolveIdempotencyKey as resolveIdempotencyKey2,
1827
3592
  resolveSessionCurrency as resolveSessionCurrency2
1828
3593
  } from "@flopay/shared";
1829
3594
  var MAX_COUPON_CODES = 5;
1830
- function readString2(value) {
1831
- return typeof value === "string" && value.trim() ? value : void 0;
1832
- }
1833
3595
  function buildCheckoutSessionError(status, payload) {
1834
3596
  const nested = payload?.error;
1835
- const code = readString2(payload?.code) ?? readString2(nested?.code) ?? `http_${status}`;
1836
- const message = readString2(payload?.message) ?? readString2(nested?.message) ?? defaultMessageForCode(code, status);
3597
+ const code = readErrorString(payload?.code) ?? readErrorString(nested?.code) ?? `http_${status}`;
3598
+ const message = readErrorString(payload?.message) ?? readErrorString(nested?.message) ?? defaultMessageForCode(code, status);
1837
3599
  return new FloPayError7(message, "api_error", { code, statusCode: status });
1838
3600
  }
1839
3601
  function defaultMessageForCode(code, status) {
@@ -1846,7 +3608,7 @@ function defaultMessageForCode(code, status) {
1846
3608
  return `Failed to create checkout session (HTTP ${status}).`;
1847
3609
  }
1848
3610
  }
1849
- async function createCheckoutSession(options) {
3611
+ async function createCheckoutSessionCore(options, onFirstByte) {
1850
3612
  const {
1851
3613
  billingApiUrl,
1852
3614
  checkoutBaseUrl,
@@ -1886,7 +3648,7 @@ async function createCheckoutSession(options) {
1886
3648
  }
1887
3649
  const payload = {
1888
3650
  clientId,
1889
- checkoutVersion: SDK_VERSION2,
3651
+ checkoutVersion: SDK_VERSION5,
1890
3652
  successUrl,
1891
3653
  cancelUrl,
1892
3654
  currency: sessionCurrency,
@@ -1929,6 +3691,10 @@ async function createCheckoutSession(options) {
1929
3691
  body: JSON.stringify(payload),
1930
3692
  signal: controller.signal
1931
3693
  });
3694
+ try {
3695
+ onFirstByte?.(response.status);
3696
+ } catch {
3697
+ }
1932
3698
  status = response.status;
1933
3699
  try {
1934
3700
  body = await response.json();
@@ -1990,9 +3756,103 @@ async function createCheckoutSession(options) {
1990
3756
  }
1991
3757
  return { status };
1992
3758
  }
3759
+ async function createCheckoutSession(options) {
3760
+ const telemetry = beginCreateSessionTelemetry(options);
3761
+ const recordFirstByte = createFirstByteRecorder(telemetry);
3762
+ try {
3763
+ const result = await createCheckoutSessionCore(options, recordFirstByte);
3764
+ completeCreateSessionTelemetry(telemetry, result);
3765
+ return result;
3766
+ } catch (error) {
3767
+ failCreateSessionTelemetry(telemetry.reporter, error);
3768
+ throw error;
3769
+ } finally {
3770
+ finishCreateSessionTelemetry(telemetry.reporter);
3771
+ }
3772
+ }
3773
+ function createFirstByteRecorder(telemetry) {
3774
+ let recorded = false;
3775
+ return (status) => {
3776
+ if (recorded) return;
3777
+ recorded = true;
3778
+ const statusClass = `${Math.floor(status / 100)}xx`;
3779
+ telemetry.reporter.log({
3780
+ name: "session.request.first_byte",
3781
+ stage: "session_first_byte",
3782
+ requestCategory: "session_create",
3783
+ statusClass
3784
+ });
3785
+ telemetry.reporter.performance({
3786
+ stage: "session_first_byte",
3787
+ durationMs: telemetry.reporter.now() - telemetry.startedAt,
3788
+ durationMode: "machine",
3789
+ requestCategory: "session_create",
3790
+ statusClass
3791
+ });
3792
+ };
3793
+ }
3794
+ function beginCreateSessionTelemetry(options) {
3795
+ const reporter = new TelemetryReporter({
3796
+ billingApiUrl: options.billingApiUrl,
3797
+ sdkVersion: SDK_VERSION5,
3798
+ enabled: options.telemetry !== false
3799
+ });
3800
+ const startedAt = reporter.now();
3801
+ reporter.log({
3802
+ name: "session.create.started",
3803
+ stage: "session_create",
3804
+ requestCategory: "session_create"
3805
+ });
3806
+ return { reporter, startedAt };
3807
+ }
3808
+ function completeCreateSessionTelemetry(telemetry, result) {
3809
+ const statusClass = `${Math.floor(result.status / 100)}xx`;
3810
+ telemetry.reporter.log({
3811
+ name: "session.request.completed",
3812
+ stage: "session_complete",
3813
+ requestCategory: "session_create",
3814
+ statusClass
3815
+ });
3816
+ telemetry.reporter.performance({
3817
+ stage: "session_complete",
3818
+ durationMs: telemetry.reporter.now() - telemetry.startedAt,
3819
+ durationMode: "machine",
3820
+ requestCategory: "session_create",
3821
+ statusClass
3822
+ });
3823
+ }
3824
+ function failCreateSessionTelemetry(reporter, error) {
3825
+ if (error instanceof FloPayError7 && error.type === "validation_error") {
3826
+ reporter.terminal({
3827
+ outcome: "validation_rejected",
3828
+ stage: "session_create",
3829
+ requestCategory: "session_create"
3830
+ });
3831
+ return;
3832
+ }
3833
+ const statusCode = error instanceof FloPayError7 ? error.statusCode : void 0;
3834
+ reporter.error({
3835
+ errorCode: error instanceof Error && error.name === "AbortError" ? "REQUEST_TIMEOUT" : error instanceof TypeError ? "NETWORK_REQUEST_FAILED" : "CHECKOUT_SESSION_CREATE_FAILED",
3836
+ stage: "session_create",
3837
+ requestCategory: "session_create",
3838
+ statusClass: error instanceof Error && error.name === "AbortError" ? "timeout" : statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
3839
+ });
3840
+ }
3841
+ function finishCreateSessionTelemetry(reporter) {
3842
+ void reporter.flush().catch(() => {
3843
+ }).finally(() => reporter.destroy());
3844
+ }
1993
3845
  async function createCheckoutSessionWithRetries(options) {
1994
3846
  const { maxRetries = 3, ...sessionOptions } = options;
3847
+ const telemetry = beginCreateSessionTelemetry(options);
3848
+ const recordFirstByte = createFirstByteRecorder(telemetry);
1995
3849
  if (!Number.isFinite(maxRetries) || !Number.isInteger(maxRetries) || maxRetries <= 0) {
3850
+ telemetry.reporter.terminal({
3851
+ outcome: "validation_rejected",
3852
+ stage: "session_create",
3853
+ requestCategory: "session_create"
3854
+ });
3855
+ finishCreateSessionTelemetry(telemetry.reporter);
1996
3856
  throw new Error("Number of retries must be greater than 0");
1997
3857
  }
1998
3858
  const attemptOptions = {
@@ -2002,18 +3862,31 @@ async function createCheckoutSessionWithRetries(options) {
2002
3862
  let lastErr;
2003
3863
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
2004
3864
  try {
2005
- return await createCheckoutSession(attemptOptions);
3865
+ const result = await createCheckoutSessionCore(attemptOptions, recordFirstByte);
3866
+ completeCreateSessionTelemetry(telemetry, result);
3867
+ finishCreateSessionTelemetry(telemetry.reporter);
3868
+ return result;
2006
3869
  } catch (err) {
2007
3870
  lastErr = err;
2008
3871
  const isTransportAbort = err instanceof Error && err.name === "AbortError";
2009
3872
  const isInProgressReplay = err instanceof FloPayError7 && err.code === IDEMPOTENCY_IN_PROGRESS_CODE2;
2010
3873
  if ((isTransportAbort || isInProgressReplay) && attempt < maxRetries) {
3874
+ telemetry.reporter.log({
3875
+ name: "operation.retry",
3876
+ stage: "session_create",
3877
+ requestCategory: "session_create",
3878
+ attempt: attempt + 1
3879
+ });
2011
3880
  await new Promise((r) => setTimeout(r, 100 * Math.pow(2, attempt)));
2012
3881
  continue;
2013
3882
  }
3883
+ failCreateSessionTelemetry(telemetry.reporter, err);
3884
+ finishCreateSessionTelemetry(telemetry.reporter);
2014
3885
  throw err;
2015
3886
  }
2016
3887
  }
3888
+ failCreateSessionTelemetry(telemetry.reporter, lastErr);
3889
+ finishCreateSessionTelemetry(telemetry.reporter);
2017
3890
  throw lastErr ?? new Error("Unknown error during checkout session creation");
2018
3891
  }
2019
3892
  export {