@flopay/js 1.3.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -45,7 +45,7 @@ __export(index_exports, {
45
45
  module.exports = __toCommonJS(index_exports);
46
46
 
47
47
  // src/load.ts
48
- var import_shared6 = require("@flopay/shared");
48
+ var import_shared7 = require("@flopay/shared");
49
49
 
50
50
  // src/stripe-adapter.ts
51
51
  var import_shared = require("@flopay/shared");
@@ -71,6 +71,11 @@ function toStripeAppearanceTheme(theme) {
71
71
  return "stripe";
72
72
  }
73
73
  }
74
+ var CARD_THREE_DS_ATTEMPT_TTL_MS = 15 * 6e4;
75
+ var MAX_CARD_THREE_DS_ATTEMPTS = 32;
76
+ function cardThreeDsNow() {
77
+ return globalThis.performance?.now() ?? Date.now();
78
+ }
74
79
  function wrapStripeElement(stripeElement) {
75
80
  const el = stripeElement;
76
81
  return {
@@ -99,6 +104,8 @@ var StripeAdapter = class {
99
104
  this.name = "stripe";
100
105
  this.stripe = null;
101
106
  this.elements = null;
107
+ this.pendingCardThreeDsAttempts = /* @__PURE__ */ new Map();
108
+ this.cardThreeDsAttemptSequence = 0;
102
109
  // Serialized appearance currently applied to `this.elements`. Used to detect
103
110
  // when consumers swap themes mid-session so we can live-update the Stripe
104
111
  // Elements group instead of returning a stale-styled cache. `null` while no
@@ -279,41 +286,84 @@ var StripeAdapter = class {
279
286
  { payment_method: params.paymentMethodId }
280
287
  );
281
288
  if (setupError) {
282
- return {
289
+ return this.withCardThreeDsLifecycle(params, {
283
290
  status: "failed",
284
291
  error: new import_shared.FloPayError(
285
292
  setupError.message ?? "Payment failed",
286
293
  "api_error",
287
294
  { code: setupError.code, declineCode: setupError.decline_code }
288
295
  )
289
- };
296
+ });
290
297
  }
291
- return {
298
+ return this.withCardThreeDsLifecycle(params, {
292
299
  status: setupIntent?.status ?? "failed",
293
300
  paymentIntentId: setupIntent?.id,
294
301
  paymentMethodId: this.extractPaymentMethodId(setupIntent?.payment_method)
295
- };
302
+ });
296
303
  }
297
304
  const { error, paymentIntent } = await this.stripe.confirmCardPayment(
298
305
  params.clientSecret,
299
306
  { payment_method: params.paymentMethodId }
300
307
  );
301
308
  if (error) {
302
- return {
309
+ return this.withCardThreeDsLifecycle(params, {
303
310
  status: "failed",
304
311
  error: new import_shared.FloPayError(
305
312
  error.message ?? "Payment failed",
306
313
  "api_error",
307
314
  { code: error.code, declineCode: error.decline_code }
308
315
  )
309
- };
316
+ });
310
317
  }
311
- return {
318
+ return this.withCardThreeDsLifecycle(params, {
312
319
  status: paymentIntent?.status ?? "failed",
313
320
  paymentIntentId: paymentIntent?.id,
314
321
  paymentMethodId: this.extractPaymentMethodId(paymentIntent?.payment_method)
322
+ });
323
+ }
324
+ /**
325
+ * Bind provider-observed 3DS milestones to the exact confirmation context.
326
+ * The sensitive client secret/payment method pair stays only in this private,
327
+ * in-memory key; callers and telemetry receive an unrelated opaque id.
328
+ */
329
+ withCardThreeDsLifecycle(params, result) {
330
+ const contextKey = `${params.clientSecret}\0${params.paymentMethodId}`;
331
+ const now = cardThreeDsNow();
332
+ this.pruneExpiredCardThreeDsAttempts(now);
333
+ if (result.status === "requires_action") {
334
+ let attempt2 = this.pendingCardThreeDsAttempts.get(contextKey);
335
+ if (!attempt2) {
336
+ while (this.pendingCardThreeDsAttempts.size >= MAX_CARD_THREE_DS_ATTEMPTS) {
337
+ const oldestContextKey = this.pendingCardThreeDsAttempts.keys().next().value;
338
+ if (oldestContextKey === void 0) break;
339
+ this.pendingCardThreeDsAttempts.delete(oldestContextKey);
340
+ }
341
+ attempt2 = {
342
+ attemptId: `card_3ds_${this.cardThreeDsAttemptSequence++}`,
343
+ startedAt: now
344
+ };
345
+ this.pendingCardThreeDsAttempts.set(contextKey, attempt2);
346
+ }
347
+ return { ...result, threeDs: { attemptId: attempt2.attemptId, status: "handoff" } };
348
+ }
349
+ const attempt = this.pendingCardThreeDsAttempts.get(contextKey);
350
+ if (!attempt) return result;
351
+ this.pendingCardThreeDsAttempts.delete(contextKey);
352
+ return {
353
+ ...result,
354
+ threeDs: {
355
+ attemptId: attempt.attemptId,
356
+ status: result.error || result.status === "failed" ? "failed" : "returned"
357
+ }
315
358
  };
316
359
  }
360
+ pruneExpiredCardThreeDsAttempts(now) {
361
+ for (const [contextKey, attempt] of this.pendingCardThreeDsAttempts) {
362
+ if (now - attempt.startedAt >= CARD_THREE_DS_ATTEMPT_TTL_MS) {
363
+ this.pendingCardThreeDsAttempts.delete(contextKey);
364
+ }
365
+ }
366
+ }
317
367
  async confirmPayment(params) {
318
368
  if (!this.stripe || !this.elements) {
319
369
  throw new import_shared.FloPayError(
@@ -500,6 +550,7 @@ var StripeAdapter = class {
500
550
  return this.stripe.elements(elementsOptions);
501
551
  }
502
552
  destroy() {
553
+ this.pendingCardThreeDsAttempts.clear();
503
554
  this.elements = null;
504
555
  this.appliedAppearanceKey = null;
505
556
  this.stripe = null;
@@ -507,7 +558,7 @@ var StripeAdapter = class {
507
558
  };
508
559
 
509
560
  // src/flopay.ts
510
- var import_shared5 = require("@flopay/shared");
561
+ var import_shared6 = require("@flopay/shared");
511
562
 
512
563
  // src/elements.ts
513
564
  var import_shared2 = require("@flopay/shared");
@@ -559,7 +610,7 @@ var FloPayElements = class {
559
610
  };
560
611
 
561
612
  // src/payment-api.ts
562
- var import_shared3 = require("@flopay/shared");
613
+ var import_shared4 = require("@flopay/shared");
563
614
 
564
615
  // src/session-display-cache.ts
565
616
  var STORAGE_KEY_PREFIX = "flopay_session_display:";
@@ -627,6 +678,334 @@ function clearSessionDisplayData(sessionId) {
627
678
  }
628
679
  }
629
680
 
681
+ // src/telemetry-reporter.ts
682
+ var import_shared3 = require("@flopay/shared");
683
+ var TELEMETRY_PATH = "/v1/sdk-telemetry/events";
684
+ var MAX_BATCH_SIZE = 16;
685
+ var MAX_QUEUE_SIZE = 64;
686
+ var UPLOAD_TIMEOUT_MS = 1500;
687
+ var ERROR_DEDUPLICATION_WINDOW_MS = 1e3;
688
+ var MAX_REPORTED_FAILURES = 64;
689
+ var DEDUPLICATION_EVENT_ID = "00000000-0000-4000-8000-000000000000";
690
+ var EVENT_BUDGETS = {
691
+ technical_error: 8,
692
+ lifecycle: 32,
693
+ expected_outcome: 32,
694
+ performance: 24
695
+ };
696
+ function createUuidV4() {
697
+ try {
698
+ return globalThis.crypto.randomUUID();
699
+ } catch {
700
+ const bytes = new Uint8Array(16);
701
+ try {
702
+ globalThis.crypto.getRandomValues(bytes);
703
+ } catch {
704
+ for (let index = 0; index < bytes.length; index += 1) {
705
+ bytes[index] = Math.floor(Math.random() * 256);
706
+ }
707
+ }
708
+ bytes[6] = bytes[6] & 15 | 64;
709
+ bytes[8] = bytes[8] & 63 | 128;
710
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
711
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
712
+ }
713
+ }
714
+ function bodyByteLength(body) {
715
+ try {
716
+ return new TextEncoder().encode(body).byteLength;
717
+ } catch {
718
+ return body.length;
719
+ }
720
+ }
721
+ function failureDeduplicationKey(event) {
722
+ return JSON.stringify([
723
+ event.code,
724
+ event.stage,
725
+ event.provider,
726
+ event.attempt,
727
+ event.statusClass,
728
+ event.requestCategory,
729
+ event.paymentMethodCategory,
730
+ event.checkoutMode,
731
+ event.layout
732
+ ]);
733
+ }
734
+ function telemetryNow() {
735
+ return globalThis.performance?.now() ?? 0;
736
+ }
737
+ var TelemetryReporter = class {
738
+ constructor(options) {
739
+ this.ingestionDisabled = false;
740
+ this.queue = [];
741
+ this.sequence = 0;
742
+ this.flushTimer = null;
743
+ this.flushInFlight = null;
744
+ this.reportedFailures = /* @__PURE__ */ new Map();
745
+ this.checkoutContext = {};
746
+ this.checkoutStartedAt = null;
747
+ this.destroyed = false;
748
+ this.pageExitHandler = () => {
749
+ this.drainQueue();
750
+ };
751
+ this.visibilityHandler = () => {
752
+ if (document.visibilityState === "hidden") void this.flush();
753
+ };
754
+ this.eventCounts = {
755
+ technical_error: 0,
756
+ lifecycle: 0,
757
+ expected_outcome: 0,
758
+ performance: 0
759
+ };
760
+ this.endpoint = `${options.billingApiUrl.replace(/\/+$/, "")}${TELEMETRY_PATH}`;
761
+ this.sdkPackage = options.sdkPackage ?? "@flopay/js";
762
+ this.sdkVersion = options.sdkVersion;
763
+ this.correlationId = createUuidV4();
764
+ this.merchantEnabled = options.enabled !== false;
765
+ this.clock = options.clock ?? telemetryNow;
766
+ this.browserTransportAvailable = typeof window !== "undefined" && typeof document !== "undefined";
767
+ if (this.browserTransportAvailable) {
768
+ window.addEventListener("pagehide", this.pageExitHandler);
769
+ document.addEventListener("visibilitychange", this.visibilityHandler);
770
+ }
771
+ }
772
+ log(input) {
773
+ if (!this.canCollect()) return;
774
+ this.enqueue((0, import_shared3.buildTelemetryLogEvent)({
775
+ ...this.checkoutContext,
776
+ ...input,
777
+ eventId: createUuidV4(),
778
+ sequence: this.sequence++
779
+ }));
780
+ }
781
+ error(input) {
782
+ if (!this.canCollect()) return;
783
+ const normalizedFailure = (0, import_shared3.buildTelemetryErrorEvent)({
784
+ ...this.checkoutContext,
785
+ ...input,
786
+ eventId: DEDUPLICATION_EVENT_ID,
787
+ sequence: 0
788
+ });
789
+ const deduplicationKey = failureDeduplicationKey(normalizedFailure);
790
+ const now = this.now();
791
+ this.pruneReportedFailures(now);
792
+ const previouslyReportedAt = this.reportedFailures.get(deduplicationKey);
793
+ if (previouslyReportedAt !== void 0 && now >= previouslyReportedAt && now - previouslyReportedAt < ERROR_DEDUPLICATION_WINDOW_MS) {
794
+ this.log({
795
+ name: "operation.deduplicated",
796
+ stage: input.stage,
797
+ provider: input.provider,
798
+ paymentMethodCategory: input.paymentMethodCategory,
799
+ attempt: input.attempt
800
+ });
801
+ return;
802
+ }
803
+ this.rememberReportedFailure(deduplicationKey, now);
804
+ this.enqueue((0, import_shared3.buildTelemetryErrorEvent)({
805
+ ...this.checkoutContext,
806
+ ...input,
807
+ eventId: createUuidV4(),
808
+ sequence: this.sequence++
809
+ }));
810
+ }
811
+ performance(input) {
812
+ if (!this.canCollect()) return;
813
+ this.enqueue((0, import_shared3.buildTelemetryPerformanceEvent)({
814
+ ...this.checkoutContext,
815
+ ...input,
816
+ eventId: createUuidV4(),
817
+ sequence: this.sequence++
818
+ }));
819
+ }
820
+ terminal(input) {
821
+ if (!this.canCollect()) return;
822
+ this.enqueue((0, import_shared3.buildTelemetryTerminalEvent)({
823
+ ...this.checkoutContext,
824
+ ...input,
825
+ eventId: createUuidV4(),
826
+ sequence: this.sequence++
827
+ }));
828
+ if (input.outcome !== "action_required" && this.checkoutStartedAt !== null) {
829
+ const checkoutStartedAt = this.checkoutStartedAt;
830
+ this.checkoutStartedAt = null;
831
+ this.performance({
832
+ stage: "total_journey",
833
+ durationMs: Math.max(0, this.now() - checkoutStartedAt),
834
+ durationMode: "total",
835
+ provider: input.provider,
836
+ paymentMethodCategory: input.paymentMethodCategory
837
+ });
838
+ }
839
+ }
840
+ /** @internal Read the reporter's monotonic clock without Resource Timing. */
841
+ now() {
842
+ try {
843
+ return this.clock();
844
+ } catch {
845
+ return telemetryNow();
846
+ }
847
+ }
848
+ pruneReportedFailures(now) {
849
+ for (const [key, reportedAt] of this.reportedFailures) {
850
+ if (now < reportedAt || now - reportedAt >= ERROR_DEDUPLICATION_WINDOW_MS) {
851
+ this.reportedFailures.delete(key);
852
+ }
853
+ }
854
+ }
855
+ rememberReportedFailure(key, reportedAt) {
856
+ while (this.reportedFailures.size >= MAX_REPORTED_FAILURES) {
857
+ const oldest = this.reportedFailures.keys().next();
858
+ if (oldest.done) break;
859
+ this.reportedFailures.delete(oldest.value);
860
+ }
861
+ this.reportedFailures.set(key, reportedAt);
862
+ }
863
+ /** @internal Add closed checkout dimensions to subsequent SDK events. */
864
+ setCheckoutContext(context) {
865
+ this.checkoutContext = {
866
+ checkoutMode: context.checkoutMode,
867
+ layout: context.layout
868
+ };
869
+ }
870
+ /** @internal Start a fresh checkout budget, dedupe window, and total span. */
871
+ beginCheckout(context = {}) {
872
+ if (!this.canCollect()) return 0;
873
+ this.drainQueue();
874
+ this.setCheckoutContext(context);
875
+ this.sequence = 0;
876
+ this.reportedFailures.clear();
877
+ this.eventCounts = {
878
+ technical_error: 0,
879
+ lifecycle: 0,
880
+ expected_outcome: 0,
881
+ performance: 0
882
+ };
883
+ this.checkoutStartedAt = this.now();
884
+ return this.checkoutStartedAt;
885
+ }
886
+ enqueue(event) {
887
+ if (!this.canCollect()) return;
888
+ if (this.queue.length >= MAX_QUEUE_SIZE || this.eventCounts[event.class] >= EVENT_BUDGETS[event.class]) return;
889
+ this.eventCounts[event.class] += 1;
890
+ this.queue.push(event);
891
+ if (this.queue.length >= MAX_BATCH_SIZE) {
892
+ void this.flush();
893
+ return;
894
+ }
895
+ this.scheduleFlush();
896
+ }
897
+ canCollect() {
898
+ return this.browserTransportAvailable && this.merchantEnabled && !this.ingestionDisabled && !this.destroyed;
899
+ }
900
+ /** Flush one bounded batch. Failures are intentionally dropped. */
901
+ async flush() {
902
+ if (this.flushInFlight) return this.flushInFlight;
903
+ if (!this.browserTransportAvailable || this.destroyed || this.ingestionDisabled || this.queue.length === 0) return;
904
+ this.clearFlushTimer();
905
+ const events = this.queue.splice(0, MAX_BATCH_SIZE);
906
+ this.flushInFlight = this.sendBatch(events).finally(() => {
907
+ this.flushInFlight = null;
908
+ if (this.queue.length > 0) this.scheduleFlush();
909
+ });
910
+ return this.flushInFlight;
911
+ }
912
+ /** Flush pending work and detach browser lifecycle listeners. */
913
+ destroy() {
914
+ if (this.destroyed) return;
915
+ this.drainQueue();
916
+ this.destroyed = true;
917
+ this.clearFlushTimer();
918
+ if (this.browserTransportAvailable) {
919
+ window.removeEventListener("pagehide", this.pageExitHandler);
920
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
921
+ }
922
+ this.queue.splice(0);
923
+ this.reportedFailures.clear();
924
+ }
925
+ /** Permanently honor a merchant opt-out and discard queued events. */
926
+ disable() {
927
+ this.merchantEnabled = false;
928
+ this.queue.splice(0);
929
+ this.reportedFailures.clear();
930
+ this.clearFlushTimer();
931
+ }
932
+ /** Start every bounded keepalive request synchronously before page teardown. */
933
+ drainQueue() {
934
+ if (!this.browserTransportAvailable || this.destroyed || this.ingestionDisabled || this.queue.length === 0) return;
935
+ this.clearFlushTimer();
936
+ while (this.queue.length > 0) {
937
+ const events = this.queue.splice(0, MAX_BATCH_SIZE);
938
+ void this.sendBatch(events);
939
+ }
940
+ }
941
+ async sendBatch(events) {
942
+ if (!this.browserTransportAvailable || this.ingestionDisabled) return;
943
+ const body = (0, import_shared3.serializeTelemetryBatch)(events, {
944
+ correlationId: this.correlationId,
945
+ sdkPackage: this.sdkPackage,
946
+ sdkVersion: this.sdkVersion,
947
+ batchId: createUuidV4()
948
+ });
949
+ if (bodyByteLength(body) > import_shared3.TELEMETRY_MAX_BATCH_BYTES) return;
950
+ const controller = typeof AbortController === "undefined" ? null : new AbortController();
951
+ let timeout = null;
952
+ try {
953
+ const request = fetch(this.endpoint, {
954
+ method: "POST",
955
+ headers: { "content-type": "text/plain;charset=UTF-8" },
956
+ body,
957
+ credentials: "omit",
958
+ keepalive: true,
959
+ referrerPolicy: "no-referrer",
960
+ signal: controller?.signal
961
+ }).then(async (response) => {
962
+ if (response.status !== 202) return null;
963
+ const payload = await response.json().catch(() => null);
964
+ return payload?.status === "disabled" ? "disabled" : null;
965
+ }).catch(() => null);
966
+ const expired = new Promise((resolve) => {
967
+ timeout = setTimeout(() => {
968
+ controller?.abort();
969
+ resolve(null);
970
+ }, UPLOAD_TIMEOUT_MS);
971
+ });
972
+ const status = await Promise.race([request, expired]);
973
+ if (status === "disabled") this.disableFromIngestion();
974
+ } catch {
975
+ } finally {
976
+ if (timeout) clearTimeout(timeout);
977
+ }
978
+ }
979
+ disableFromIngestion() {
980
+ this.ingestionDisabled = true;
981
+ this.queue.splice(0);
982
+ this.reportedFailures.clear();
983
+ this.clearFlushTimer();
984
+ }
985
+ scheduleFlush() {
986
+ if (this.flushTimer || this.destroyed || this.ingestionDisabled) return;
987
+ this.flushTimer = setTimeout(() => {
988
+ this.flushTimer = null;
989
+ void this.flush();
990
+ }, 0);
991
+ }
992
+ clearFlushTimer() {
993
+ if (!this.flushTimer) return;
994
+ clearTimeout(this.flushTimer);
995
+ this.flushTimer = null;
996
+ }
997
+ };
998
+ var TELEMETRY_REPORTER_FACTORY = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.reporter-factory.v1");
999
+ var telemetryGlobal = globalThis;
1000
+ if (telemetryGlobal[TELEMETRY_REPORTER_FACTORY] === void 0) {
1001
+ Object.defineProperty(telemetryGlobal, TELEMETRY_REPORTER_FACTORY, {
1002
+ configurable: true,
1003
+ enumerable: false,
1004
+ writable: false,
1005
+ value: (options) => new TelemetryReporter(options)
1006
+ });
1007
+ }
1008
+
630
1009
  // src/payment-api.ts
631
1010
  var DEFAULT_PROCESSING_RETRY_AFTER_MS = 1e3;
632
1011
  var MIN_PROCESSING_RETRY_AFTER_MS = 500;
@@ -636,6 +1015,25 @@ var DEFAULT_ACCOUNT_SNAPSHOT_TIMEOUT_MS = 1e4;
636
1015
  function isRecord(value) {
637
1016
  return typeof value === "object" && value !== null;
638
1017
  }
1018
+ function telemetryStatusClass(status) {
1019
+ if (status === void 0) return "network_error";
1020
+ const statusClass = `${Math.floor(status / 100)}xx`;
1021
+ return statusClass === "2xx" || statusClass === "3xx" || statusClass === "4xx" || statusClass === "5xx" ? statusClass : "unknown";
1022
+ }
1023
+ function telemetryFailure(error, fallbackCode) {
1024
+ if (error instanceof Error && (error.name === "AbortError" || error instanceof import_shared4.FloPayError && error.code === "checkout_processing_timeout")) {
1025
+ return { errorCode: "REQUEST_TIMEOUT", statusClass: "timeout" };
1026
+ }
1027
+ if (error instanceof TypeError) {
1028
+ return { errorCode: "NETWORK_REQUEST_FAILED", statusClass: "network_error" };
1029
+ }
1030
+ return {
1031
+ errorCode: fallbackCode,
1032
+ statusClass: telemetryStatusClass(
1033
+ error instanceof import_shared4.FloPayError ? error.statusCode : void 0
1034
+ )
1035
+ };
1036
+ }
639
1037
  function readString(payload, key) {
640
1038
  const value = payload?.[key];
641
1039
  return typeof value === "string" && value.trim() ? value : void 0;
@@ -657,7 +1055,7 @@ function delay(ms) {
657
1055
  return new Promise((resolve) => setTimeout(resolve, ms));
658
1056
  }
659
1057
  function createCheckoutProcessingTimeoutError() {
660
- return new import_shared3.FloPayError(
1058
+ return new import_shared4.FloPayError(
661
1059
  "Checkout is still processing. Please try again shortly.",
662
1060
  "api_error",
663
1061
  { code: "checkout_processing_timeout" }
@@ -668,14 +1066,14 @@ async function buildApiErrorFromResponse(response, fallbackMessage) {
668
1066
  const nestedError = isRecord(payload?.error) ? payload.error : null;
669
1067
  const message = readMessage(payload, "message") ?? readMessage(nestedError, "message") ?? fallbackMessage;
670
1068
  const code = readString(payload, "code") ?? readString(payload, "gatewayErrorCode") ?? readString(nestedError, "code") ?? `http_${response.status}`;
671
- return new import_shared3.FloPayError(message, "api_error", {
1069
+ return new import_shared4.FloPayError(message, "api_error", {
672
1070
  code,
673
1071
  statusCode: response.status
674
1072
  });
675
1073
  }
676
1074
  var NETWORK_RETRY_ATTEMPTS = 2;
677
1075
  var IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS = 2;
678
- async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEMPTS) {
1076
+ async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEMPTS, onRetry) {
679
1077
  let lastErr;
680
1078
  for (let attempt = 0; ; attempt++) {
681
1079
  try {
@@ -684,13 +1082,59 @@ async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEM
684
1082
  if (err instanceof Error && err.name === "AbortError") throw err;
685
1083
  lastErr = err;
686
1084
  if (attempt >= attempts) throw lastErr;
1085
+ try {
1086
+ onRetry?.(attempt + 1);
1087
+ } catch {
1088
+ }
687
1089
  await delay(150 * 2 ** attempt);
688
1090
  }
689
1091
  }
690
1092
  }
1093
+ function isPaymentApiTelemetryHooks(value) {
1094
+ return "now" in value || "onFirstByte" in value || "onSessionCreateFailure" in value || "onRetry" in value;
1095
+ }
691
1096
  var PaymentAPI = class {
692
- constructor(billingApiUrl) {
1097
+ constructor(billingApiUrl, telemetryOptionsOrHooks = {}) {
693
1098
  this.baseUrl = billingApiUrl.replace(/\/+$/, "");
1099
+ const hasInternalHooks = isPaymentApiTelemetryHooks(telemetryOptionsOrHooks);
1100
+ this.telemetryHooks = hasInternalHooks ? telemetryOptionsOrHooks : void 0;
1101
+ this.directTelemetry = hasInternalHooks || telemetryOptionsOrHooks.telemetry === false ? void 0 : new TelemetryReporter({
1102
+ billingApiUrl: this.baseUrl,
1103
+ sdkVersion: import_shared4.SDK_VERSION
1104
+ });
1105
+ }
1106
+ /** Dispose the reporter owned by direct public usage. Internal hooks are never disposed here. */
1107
+ destroy() {
1108
+ this.directTelemetry?.destroy();
1109
+ }
1110
+ reportDirectFailure(error, fallbackCode, stage, requestCategory, paymentMethodCategory = "unknown") {
1111
+ const failure = telemetryFailure(error, fallbackCode);
1112
+ this.directTelemetry?.error({
1113
+ ...failure,
1114
+ stage,
1115
+ requestCategory,
1116
+ paymentMethodCategory
1117
+ });
1118
+ }
1119
+ telemetryTimestamp() {
1120
+ try {
1121
+ return this.telemetryHooks?.now?.() ?? this.directTelemetry?.now() ?? telemetryNow();
1122
+ } catch {
1123
+ return telemetryNow();
1124
+ }
1125
+ }
1126
+ beginDirectTelemetryCheckout(checkoutSessionId) {
1127
+ if (!this.directTelemetry || this.directTelemetryCheckoutId === checkoutSessionId) return;
1128
+ this.directTelemetryCheckoutId = checkoutSessionId;
1129
+ this.directTelemetry.beginCheckout();
1130
+ }
1131
+ beginDirectTelemetryOperation() {
1132
+ if (!this.directTelemetry) return;
1133
+ this.directTelemetryCheckoutId = void 0;
1134
+ this.directTelemetry.beginCheckout();
1135
+ }
1136
+ adoptDirectTelemetryCheckout(checkoutSessionId) {
1137
+ if (checkoutSessionId) this.directTelemetryCheckoutId = checkoutSessionId;
694
1138
  }
695
1139
  /**
696
1140
  * Fetch a raw checkout session by ID.
@@ -702,17 +1146,77 @@ var PaymentAPI = class {
702
1146
  * Backends that don't yet enforce it ignore the extra header.
703
1147
  */
704
1148
  async getCheckoutSession(checkoutSessionId, nonce) {
705
- const headers = { [import_shared3.FLO_SDK_VERSION_HEADER]: import_shared3.SDK_VERSION };
1149
+ this.beginDirectTelemetryCheckout(checkoutSessionId);
1150
+ const requestStarted = this.telemetryTimestamp();
1151
+ this.directTelemetry?.log({
1152
+ name: "session.read.started",
1153
+ stage: "session_read",
1154
+ requestCategory: "session_read"
1155
+ });
1156
+ const headers = { [import_shared4.FLO_SDK_VERSION_HEADER]: import_shared4.SDK_VERSION };
706
1157
  if (nonce) headers["x-checkout-session-token"] = nonce;
707
- const response = await fetchWithNetworkRetry(
708
- `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}`,
709
- { headers }
710
- );
711
- if (!response.ok) {
712
- throw await buildApiErrorFromResponse(response, "Failed to get checkout session");
1158
+ try {
1159
+ const response = await fetchWithNetworkRetry(
1160
+ `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}`,
1161
+ { headers },
1162
+ NETWORK_RETRY_ATTEMPTS,
1163
+ (attempt) => {
1164
+ this.telemetryHooks?.onRetry?.("session_read", attempt);
1165
+ this.directTelemetry?.log({
1166
+ name: "operation.retry",
1167
+ stage: "session_read",
1168
+ requestCategory: "session_read",
1169
+ attempt
1170
+ });
1171
+ }
1172
+ );
1173
+ const firstByteDuration = Math.max(0, this.telemetryTimestamp() - requestStarted);
1174
+ try {
1175
+ this.telemetryHooks?.onFirstByte?.(firstByteDuration);
1176
+ } catch {
1177
+ }
1178
+ const statusClass = `${Math.floor(response.status / 100)}xx`;
1179
+ this.directTelemetry?.log({
1180
+ name: "session.request.first_byte",
1181
+ stage: "session_first_byte",
1182
+ requestCategory: "session_read",
1183
+ statusClass
1184
+ });
1185
+ this.directTelemetry?.performance({
1186
+ stage: "session_first_byte",
1187
+ durationMs: firstByteDuration,
1188
+ durationMode: "machine",
1189
+ requestCategory: "session_read",
1190
+ statusClass
1191
+ });
1192
+ if (!response.ok) {
1193
+ throw await buildApiErrorFromResponse(response, "Failed to get checkout session");
1194
+ }
1195
+ const body = await response.json();
1196
+ this.directTelemetry?.log({
1197
+ name: "session.request.completed",
1198
+ stage: "session_complete",
1199
+ requestCategory: "session_read",
1200
+ statusClass
1201
+ });
1202
+ this.directTelemetry?.performance({
1203
+ stage: "session_complete",
1204
+ durationMs: Math.max(0, this.telemetryTimestamp() - requestStarted),
1205
+ durationMode: "machine",
1206
+ requestCategory: "session_read",
1207
+ statusClass
1208
+ });
1209
+ return { ...body, data: this.mergeCachedDisplayData(body.data) };
1210
+ } catch (error) {
1211
+ const statusCode = error instanceof import_shared4.FloPayError ? error.statusCode : void 0;
1212
+ this.directTelemetry?.error({
1213
+ errorCode: error instanceof import_shared4.FloPayError && error.code === "checkout_processing_timeout" ? "REQUEST_TIMEOUT" : "NETWORK_REQUEST_FAILED",
1214
+ stage: "session_read",
1215
+ requestCategory: "session_read",
1216
+ statusClass: statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
1217
+ });
1218
+ throw error;
713
1219
  }
714
- const body = await response.json();
715
- return { ...body, data: this.mergeCachedDisplayData(body.data) };
716
1220
  }
717
1221
  /**
718
1222
  * Stash display-only data for a session so subsequent fetches can fill in
@@ -768,17 +1272,55 @@ var PaymentAPI = class {
768
1272
  * backends, matched against the session's stored nonce).
769
1273
  */
770
1274
  async getVaultCapture(checkoutSessionId, nonce) {
1275
+ this.beginDirectTelemetryCheckout(checkoutSessionId);
1276
+ const startedAt = this.telemetryTimestamp();
1277
+ this.directTelemetry?.log({
1278
+ name: "vault.capture.requested",
1279
+ stage: "vault_request",
1280
+ requestCategory: "vault_capture",
1281
+ paymentMethodCategory: "card"
1282
+ });
771
1283
  const headers = { "Content-Type": "application/json" };
772
1284
  if (nonce) headers["x-checkout-session-token"] = nonce;
773
- const response = await fetchWithNetworkRetry(
774
- `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}/vault/capture`,
775
- { method: "POST", headers }
776
- );
777
- if (!response.ok) {
778
- throw await buildApiErrorFromResponse(response, "Failed to load the secure card form");
1285
+ try {
1286
+ const response = await fetchWithNetworkRetry(
1287
+ `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}/vault/capture`,
1288
+ { method: "POST", headers },
1289
+ NETWORK_RETRY_ATTEMPTS,
1290
+ (attempt) => {
1291
+ this.telemetryHooks?.onRetry?.("vault_capture", attempt);
1292
+ this.directTelemetry?.log({
1293
+ name: "operation.retry",
1294
+ stage: "vault_request",
1295
+ requestCategory: "vault_capture",
1296
+ paymentMethodCategory: "card",
1297
+ attempt
1298
+ });
1299
+ }
1300
+ );
1301
+ if (!response.ok) {
1302
+ throw await buildApiErrorFromResponse(response, "Failed to load the secure card form");
1303
+ }
1304
+ const block = await response.json();
1305
+ this.directTelemetry?.performance({
1306
+ stage: "vault_request",
1307
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1308
+ durationMode: "machine",
1309
+ requestCategory: "vault_capture",
1310
+ paymentMethodCategory: "card",
1311
+ statusClass: "2xx"
1312
+ });
1313
+ return this.toVaultBlock(block);
1314
+ } catch (error) {
1315
+ this.reportDirectFailure(
1316
+ error,
1317
+ "VAULT_LOAD_FAILED",
1318
+ "vault_request",
1319
+ "vault_capture",
1320
+ "card"
1321
+ );
1322
+ throw error;
779
1323
  }
780
- const block = await response.json();
781
- return this.toVaultBlock(block);
782
1324
  }
783
1325
  /**
784
1326
  * Fetch and normalize a checkout session.
@@ -814,26 +1356,88 @@ var PaymentAPI = class {
814
1356
  * future major version.
815
1357
  */
816
1358
  async processPayment(_userId, data, options) {
1359
+ this.beginDirectTelemetryCheckout(data.sessionId);
817
1360
  if (!data.nonce) {
818
- throw new import_shared3.FloPayError(
1361
+ this.directTelemetry?.terminal({
1362
+ outcome: "validation_rejected",
1363
+ stage: "processing",
1364
+ requestCategory: "process_payment"
1365
+ });
1366
+ throw new import_shared4.FloPayError(
819
1367
  "processPayment requires `nonce` \u2014 pass the value returned from session creation.",
820
1368
  "validation_error",
821
1369
  { code: "MissingCheckoutSessionToken", param: "nonce" }
822
1370
  );
823
1371
  }
1372
+ const startedAt = this.telemetryTimestamp();
1373
+ this.directTelemetry?.log({
1374
+ name: "payment.processing.started",
1375
+ stage: "processing",
1376
+ requestCategory: "process_payment"
1377
+ });
824
1378
  const { nonce, ...processBody } = data;
825
- const response = await fetch(
826
- `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(data.sessionId)}/process`,
827
- {
828
- method: "POST",
829
- headers: {
830
- "Content-Type": "application/json",
831
- "x-checkout-session-token": nonce
832
- },
833
- body: JSON.stringify(processBody)
1379
+ let response;
1380
+ try {
1381
+ response = await fetch(
1382
+ `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(data.sessionId)}/process`,
1383
+ {
1384
+ method: "POST",
1385
+ headers: {
1386
+ "Content-Type": "application/json",
1387
+ "x-checkout-session-token": nonce
1388
+ },
1389
+ body: JSON.stringify(processBody)
1390
+ }
1391
+ );
1392
+ } catch (error) {
1393
+ this.reportDirectFailure(
1394
+ error,
1395
+ "PAYMENT_PROCESSING_FAILED",
1396
+ "processing",
1397
+ "process_payment"
1398
+ );
1399
+ throw error;
1400
+ }
1401
+ if (!response.ok && response.status !== 202) {
1402
+ this.directTelemetry?.error({
1403
+ errorCode: "PAYMENT_PROCESSING_FAILED",
1404
+ stage: "processing",
1405
+ requestCategory: "process_payment",
1406
+ statusClass: telemetryStatusClass(response.status)
1407
+ });
1408
+ return response;
1409
+ }
1410
+ try {
1411
+ const result = await this.resolveProcessResponse(
1412
+ response,
1413
+ data.sessionId,
1414
+ { ...options, nonce }
1415
+ );
1416
+ this.directTelemetry?.log({
1417
+ name: "payment.processing.completed",
1418
+ stage: "processing",
1419
+ requestCategory: "process_payment",
1420
+ statusClass: telemetryStatusClass(result.status)
1421
+ });
1422
+ this.directTelemetry?.performance({
1423
+ stage: "processing",
1424
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1425
+ durationMode: "machine",
1426
+ requestCategory: "process_payment",
1427
+ statusClass: telemetryStatusClass(result.status)
1428
+ });
1429
+ return result;
1430
+ } catch (error) {
1431
+ if (!(error instanceof import_shared4.FloPayError && error.code === "checkout_processing_timeout")) {
1432
+ this.reportDirectFailure(
1433
+ error,
1434
+ "PAYMENT_PROCESSING_FAILED",
1435
+ "processing",
1436
+ "process_payment"
1437
+ );
834
1438
  }
835
- );
836
- return this.resolveProcessResponse(response, data.sessionId, { ...options, nonce });
1439
+ throw error;
1440
+ }
837
1441
  }
838
1442
  /**
839
1443
  * Patch the buyer's account snapshot (email, name, billing address, AVS
@@ -857,6 +1461,13 @@ var PaymentAPI = class {
857
1461
  * AVS-protected charge to decline downstream.
858
1462
  */
859
1463
  async patchAccountSnapshot(sessionId, nonce, body, options) {
1464
+ this.beginDirectTelemetryCheckout(sessionId);
1465
+ const startedAt = this.telemetryTimestamp();
1466
+ this.directTelemetry?.log({
1467
+ name: "operation.state_transition",
1468
+ stage: "processing",
1469
+ requestCategory: "account_snapshot"
1470
+ });
860
1471
  const timeoutMs = options?.timeoutMs ?? DEFAULT_ACCOUNT_SNAPSHOT_TIMEOUT_MS;
861
1472
  const controller = new AbortController();
862
1473
  const onCallerAbort = () => controller.abort();
@@ -865,26 +1476,53 @@ var PaymentAPI = class {
865
1476
  else options.signal.addEventListener("abort", onCallerAbort, { once: true });
866
1477
  }
867
1478
  const timer = setTimeout(() => controller.abort(), timeoutMs);
868
- let response;
869
1479
  try {
870
- response = await fetchWithNetworkRetry(
871
- `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(sessionId)}/account`,
872
- {
873
- method: "PATCH",
874
- headers: {
875
- "Content-Type": "application/json",
876
- "x-checkout-session-token": nonce
1480
+ let response;
1481
+ try {
1482
+ response = await fetchWithNetworkRetry(
1483
+ `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(sessionId)}/account`,
1484
+ {
1485
+ method: "PATCH",
1486
+ headers: {
1487
+ "Content-Type": "application/json",
1488
+ "x-checkout-session-token": nonce
1489
+ },
1490
+ body: JSON.stringify(body),
1491
+ signal: controller.signal
877
1492
  },
878
- body: JSON.stringify(body),
879
- signal: controller.signal
880
- }
1493
+ NETWORK_RETRY_ATTEMPTS,
1494
+ (attempt) => {
1495
+ this.telemetryHooks?.onRetry?.("account_snapshot", attempt);
1496
+ this.directTelemetry?.log({
1497
+ name: "operation.retry",
1498
+ stage: "processing",
1499
+ requestCategory: "account_snapshot",
1500
+ attempt
1501
+ });
1502
+ }
1503
+ );
1504
+ } finally {
1505
+ clearTimeout(timer);
1506
+ options?.signal?.removeEventListener("abort", onCallerAbort);
1507
+ }
1508
+ if (!response.ok) {
1509
+ throw await buildApiErrorFromResponse(response, "Failed to persist account snapshot");
1510
+ }
1511
+ this.directTelemetry?.performance({
1512
+ stage: "processing",
1513
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1514
+ durationMode: "machine",
1515
+ requestCategory: "account_snapshot",
1516
+ statusClass: "2xx"
1517
+ });
1518
+ } catch (error) {
1519
+ this.reportDirectFailure(
1520
+ error,
1521
+ "NETWORK_REQUEST_FAILED",
1522
+ "processing",
1523
+ "account_snapshot"
881
1524
  );
882
- } finally {
883
- clearTimeout(timer);
884
- options?.signal?.removeEventListener("abort", onCallerAbort);
885
- }
886
- if (!response.ok) {
887
- throw await buildApiErrorFromResponse(response, "Failed to persist account snapshot");
1525
+ throw error;
888
1526
  }
889
1527
  }
890
1528
  /**
@@ -906,22 +1544,62 @@ var PaymentAPI = class {
906
1544
  * passing the concrete payment method id / type.
907
1545
  */
908
1546
  async createPaymentIntent(sessionId, email, paymentMethodType, options) {
1547
+ this.beginDirectTelemetryCheckout(sessionId);
1548
+ const startedAt = this.telemetryTimestamp();
1549
+ this.directTelemetry?.log({
1550
+ name: "payment.intent.started",
1551
+ stage: "processing",
1552
+ requestCategory: "intent_create"
1553
+ });
909
1554
  const headers = { "Content-Type": "application/json" };
910
1555
  if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
911
- return fetch(
912
- `${this.baseUrl}/v1/checkouts/payments/intents`,
913
- {
914
- method: "POST",
915
- headers,
916
- body: JSON.stringify({
917
- sessionId,
918
- email,
919
- paymentMethodType: paymentMethodType ?? null,
920
- isPaypal: options?.isPaypal ?? false
921
- }),
922
- signal: options?.signal
1556
+ try {
1557
+ const response = await fetch(
1558
+ `${this.baseUrl}/v1/checkouts/payments/intents`,
1559
+ {
1560
+ method: "POST",
1561
+ headers,
1562
+ body: JSON.stringify({
1563
+ sessionId,
1564
+ email,
1565
+ paymentMethodType: paymentMethodType ?? null,
1566
+ isPaypal: options?.isPaypal ?? false
1567
+ }),
1568
+ signal: options?.signal
1569
+ }
1570
+ );
1571
+ if (!response.ok) {
1572
+ this.directTelemetry?.error({
1573
+ errorCode: "PAYMENT_PROCESSING_FAILED",
1574
+ stage: "processing",
1575
+ requestCategory: "intent_create",
1576
+ statusClass: telemetryStatusClass(response.status)
1577
+ });
1578
+ return response;
923
1579
  }
924
- );
1580
+ this.directTelemetry?.log({
1581
+ name: "payment.intent.completed",
1582
+ stage: "processing",
1583
+ requestCategory: "intent_create",
1584
+ statusClass: telemetryStatusClass(response.status)
1585
+ });
1586
+ this.directTelemetry?.performance({
1587
+ stage: "processing",
1588
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1589
+ durationMode: "machine",
1590
+ requestCategory: "intent_create",
1591
+ statusClass: telemetryStatusClass(response.status)
1592
+ });
1593
+ return response;
1594
+ } catch (error) {
1595
+ this.reportDirectFailure(
1596
+ error,
1597
+ "PAYMENT_PROCESSING_FAILED",
1598
+ "processing",
1599
+ "intent_create"
1600
+ );
1601
+ throw error;
1602
+ }
925
1603
  }
926
1604
  /**
927
1605
  * Create a SetupIntent for saving payment methods.
@@ -930,23 +1608,71 @@ var PaymentAPI = class {
930
1608
  * required by post-#640 backends, ignored by earlier versions.
931
1609
  */
932
1610
  async createSetupIntent(sessionId, email, paymentMethodType, options) {
1611
+ this.beginDirectTelemetryCheckout(sessionId);
1612
+ const startedAt = this.telemetryTimestamp();
1613
+ this.directTelemetry?.log({
1614
+ name: "payment.intent.started",
1615
+ stage: "processing",
1616
+ requestCategory: "intent_create"
1617
+ });
933
1618
  const headers = { "Content-Type": "application/json" };
934
1619
  if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
935
- return fetch(
936
- `${this.baseUrl}/v1/checkouts/payments/setup-intents`,
937
- {
938
- method: "POST",
939
- headers,
940
- body: JSON.stringify({ sessionId, email, paymentMethodType }),
941
- signal: options?.signal
1620
+ try {
1621
+ const response = await fetch(
1622
+ `${this.baseUrl}/v1/checkouts/payments/setup-intents`,
1623
+ {
1624
+ method: "POST",
1625
+ headers,
1626
+ body: JSON.stringify({ sessionId, email, paymentMethodType }),
1627
+ signal: options?.signal
1628
+ }
1629
+ );
1630
+ if (!response.ok) {
1631
+ this.directTelemetry?.error({
1632
+ errorCode: "PAYMENT_PROCESSING_FAILED",
1633
+ stage: "processing",
1634
+ requestCategory: "intent_create",
1635
+ statusClass: telemetryStatusClass(response.status)
1636
+ });
1637
+ return response;
942
1638
  }
943
- );
1639
+ this.directTelemetry?.log({
1640
+ name: "payment.intent.completed",
1641
+ stage: "processing",
1642
+ requestCategory: "intent_create",
1643
+ statusClass: telemetryStatusClass(response.status)
1644
+ });
1645
+ this.directTelemetry?.performance({
1646
+ stage: "processing",
1647
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1648
+ durationMode: "machine",
1649
+ requestCategory: "intent_create",
1650
+ statusClass: telemetryStatusClass(response.status)
1651
+ });
1652
+ return response;
1653
+ } catch (error) {
1654
+ this.reportDirectFailure(
1655
+ error,
1656
+ "PAYMENT_PROCESSING_FAILED",
1657
+ "processing",
1658
+ "intent_create"
1659
+ );
1660
+ throw error;
1661
+ }
944
1662
  }
945
1663
  /**
946
1664
  * Fetch user's prior payments by email.
947
1665
  * Used to determine if saved card UX should be shown.
948
1666
  */
949
1667
  async getPaymentsByEmail(email, options) {
1668
+ this.beginDirectTelemetryOperation();
1669
+ const startedAt = this.telemetryTimestamp();
1670
+ this.directTelemetry?.log({
1671
+ name: "operation.recovery.started",
1672
+ stage: "recovery",
1673
+ requestCategory: "other",
1674
+ paymentMethodCategory: "saved"
1675
+ });
950
1676
  const page = options?.page ?? 1;
951
1677
  const limit = options?.limit ?? 1;
952
1678
  const params = new URLSearchParams({
@@ -956,18 +1682,43 @@ var PaymentAPI = class {
956
1682
  sortField: "createdAt",
957
1683
  sortDirection: "DESC"
958
1684
  });
959
- const response = await fetch(
960
- `${this.baseUrl}/v1/payments?${params.toString()}`,
961
- {
962
- method: "GET",
963
- signal: options?.signal,
964
- keepalive: true
1685
+ try {
1686
+ const response = await fetch(
1687
+ `${this.baseUrl}/v1/payments?${params.toString()}`,
1688
+ {
1689
+ method: "GET",
1690
+ signal: options?.signal,
1691
+ keepalive: true
1692
+ }
1693
+ );
1694
+ if (!response.ok) {
1695
+ throw new import_shared4.FloPayError(
1696
+ "Failed to fetch payments",
1697
+ "api_error",
1698
+ { statusCode: response.status }
1699
+ );
965
1700
  }
966
- );
967
- if (!response.ok) {
968
- throw new import_shared3.FloPayError("Failed to fetch payments", "api_error");
1701
+ const result = await response.json();
1702
+ this.directTelemetry?.log({
1703
+ name: "operation.recovery.completed",
1704
+ stage: "recovery",
1705
+ requestCategory: "other",
1706
+ paymentMethodCategory: "saved",
1707
+ statusClass: "2xx"
1708
+ });
1709
+ this.directTelemetry?.performance({
1710
+ stage: "recovery",
1711
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1712
+ durationMode: "machine",
1713
+ requestCategory: "other",
1714
+ paymentMethodCategory: "saved",
1715
+ statusClass: "2xx"
1716
+ });
1717
+ return result;
1718
+ } catch (error) {
1719
+ this.reportDirectFailure(error, "RECOVERY_FAILED", "recovery", "other", "saved");
1720
+ throw error;
969
1721
  }
970
- return response.json();
971
1722
  }
972
1723
  /**
973
1724
  * Create a checkout session AND return the full session data in one call.
@@ -977,15 +1728,65 @@ var PaymentAPI = class {
977
1728
  * Falls back to create + GET if the backend doesn't support `expand`.
978
1729
  */
979
1730
  async createAndFetchSession(params) {
980
- const wireProducts = params.products ?? (0, import_shared3.foldIntoProducts)(params.items, params.subscriptions);
981
- const sessionCurrency = (0, import_shared3.resolveSessionCurrency)(
1731
+ this.beginDirectTelemetryOperation();
1732
+ const startedAt = this.telemetryTimestamp();
1733
+ this.directTelemetry?.log({
1734
+ name: "session.create.started",
1735
+ stage: "session_create",
1736
+ requestCategory: "session_create"
1737
+ });
1738
+ try {
1739
+ const result = await this.createAndFetchSessionRequest(params, startedAt);
1740
+ this.adoptDirectTelemetryCheckout(result.data.session?.id);
1741
+ this.directTelemetry?.log({
1742
+ name: "session.request.completed",
1743
+ stage: "session_complete",
1744
+ requestCategory: "session_create",
1745
+ statusClass: "2xx"
1746
+ });
1747
+ this.directTelemetry?.performance({
1748
+ stage: "session_create",
1749
+ durationMs: this.telemetryTimestamp() - startedAt,
1750
+ durationMode: "machine",
1751
+ requestCategory: "session_create",
1752
+ statusClass: "2xx"
1753
+ });
1754
+ return result;
1755
+ } catch (error) {
1756
+ if (!(error instanceof import_shared4.FloPayError && error.code === "session_auto_completed")) {
1757
+ try {
1758
+ this.telemetryHooks?.onSessionCreateFailure?.(error);
1759
+ } catch {
1760
+ }
1761
+ }
1762
+ if (error instanceof import_shared4.FloPayError && error.type === "validation_error") {
1763
+ this.directTelemetry?.terminal({
1764
+ outcome: "validation_rejected",
1765
+ stage: "session_create",
1766
+ requestCategory: "session_create"
1767
+ });
1768
+ } else if (!(error instanceof import_shared4.FloPayError && error.code === "session_auto_completed")) {
1769
+ const statusCode = error instanceof import_shared4.FloPayError ? error.statusCode : void 0;
1770
+ this.directTelemetry?.error({
1771
+ errorCode: error instanceof Error && error.name === "AbortError" ? "REQUEST_TIMEOUT" : error instanceof TypeError ? "NETWORK_REQUEST_FAILED" : "CHECKOUT_SESSION_CREATE_FAILED",
1772
+ stage: "session_create",
1773
+ requestCategory: "session_create",
1774
+ statusClass: error instanceof Error && error.name === "AbortError" ? "timeout" : statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
1775
+ });
1776
+ }
1777
+ throw error;
1778
+ }
1779
+ }
1780
+ async createAndFetchSessionRequest(params, telemetryStartedAt) {
1781
+ const wireProducts = params.products ?? (0, import_shared4.foldIntoProducts)(params.items, params.subscriptions);
1782
+ const sessionCurrency = (0, import_shared4.resolveSessionCurrency)(
982
1783
  params.currency,
983
1784
  params.items,
984
1785
  params.subscriptions,
985
1786
  wireProducts
986
1787
  );
987
1788
  if (!sessionCurrency) {
988
- throw new import_shared3.FloPayError(
1789
+ throw new import_shared4.FloPayError(
989
1790
  "currency is required: pass `currency` on the session, or include a `currency` on the first item/subscription/product.",
990
1791
  "validation_error",
991
1792
  { code: "CurrencyRequired", param: "currency" }
@@ -993,12 +1794,12 @@ var PaymentAPI = class {
993
1794
  }
994
1795
  const payload = {
995
1796
  clientId: params.clientId,
996
- checkoutVersion: import_shared3.SDK_VERSION,
1797
+ checkoutVersion: import_shared4.SDK_VERSION,
997
1798
  successUrl: params.successUrl,
998
1799
  cancelUrl: params.cancelUrl,
999
1800
  currency: sessionCurrency,
1000
1801
  checkoutMode: params.checkoutMode ?? "full",
1001
- products: wireProducts.map((product) => (0, import_shared3.buildProductPayload)(product, sessionCurrency)),
1802
+ products: wireProducts.map((product) => (0, import_shared4.buildProductPayload)(product, sessionCurrency)),
1002
1803
  accountData: {
1003
1804
  userId: params.account.userId,
1004
1805
  firstName: params.account.firstName ?? null,
@@ -1026,13 +1827,14 @@ var PaymentAPI = class {
1026
1827
  // Declare the SDK version so backends at TeamFloPay/backend#823 embed
1027
1828
  // the hosted vault capture block (`body.vault`) in the response for
1028
1829
  // SDKs ≥ 1.3.0. Older backends ignore the header.
1029
- [import_shared3.FLO_SDK_VERSION_HEADER]: import_shared3.SDK_VERSION
1830
+ [import_shared4.FLO_SDK_VERSION_HEADER]: import_shared4.SDK_VERSION
1030
1831
  };
1031
- const idempotencyKey = (0, import_shared3.resolveIdempotencyKey)(params.idempotencyKey);
1832
+ const idempotencyKey = (0, import_shared4.resolveIdempotencyKey)(params.idempotencyKey);
1032
1833
  if (idempotencyKey) {
1033
- headers[import_shared3.IDEMPOTENCY_KEY_HEADER] = idempotencyKey;
1834
+ headers[import_shared4.IDEMPOTENCY_KEY_HEADER] = idempotencyKey;
1034
1835
  }
1035
1836
  let response;
1837
+ let firstByteReported = false;
1036
1838
  for (let attempt = 0; ; attempt++) {
1037
1839
  response = await fetchWithNetworkRetry(
1038
1840
  `${this.baseUrl}/v1/checkouts/sessions?expand=true`,
@@ -1040,10 +1842,37 @@ var PaymentAPI = class {
1040
1842
  method: "POST",
1041
1843
  headers,
1042
1844
  body: JSON.stringify(payload)
1845
+ },
1846
+ NETWORK_RETRY_ATTEMPTS,
1847
+ (networkAttempt) => {
1848
+ this.telemetryHooks?.onRetry?.("session_create", networkAttempt);
1849
+ this.directTelemetry?.log({
1850
+ name: "operation.retry",
1851
+ stage: "session_create",
1852
+ requestCategory: "session_create",
1853
+ attempt: networkAttempt
1854
+ });
1043
1855
  }
1044
1856
  );
1857
+ if (!firstByteReported) {
1858
+ firstByteReported = true;
1859
+ const statusClass = `${Math.floor(response.status / 100)}xx`;
1860
+ this.directTelemetry?.log({
1861
+ name: "session.request.first_byte",
1862
+ stage: "session_first_byte",
1863
+ requestCategory: "session_create",
1864
+ statusClass
1865
+ });
1866
+ this.directTelemetry?.performance({
1867
+ stage: "session_first_byte",
1868
+ durationMs: this.telemetryTimestamp() - telemetryStartedAt,
1869
+ durationMode: "machine",
1870
+ requestCategory: "session_create",
1871
+ statusClass
1872
+ });
1873
+ }
1045
1874
  if (response.status === 204) {
1046
- throw new import_shared3.FloPayError(
1875
+ throw new import_shared4.FloPayError(
1047
1876
  "Session auto-completed \u2014 payment method already on file",
1048
1877
  "api_error",
1049
1878
  { code: "session_auto_completed" }
@@ -1051,7 +1880,17 @@ var PaymentAPI = class {
1051
1880
  }
1052
1881
  if (response.ok) break;
1053
1882
  const error = await buildApiErrorFromResponse(response, "Failed to create checkout session");
1054
- if (error.code === import_shared3.IDEMPOTENCY_IN_PROGRESS_CODE && attempt < IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS) {
1883
+ if (error.code === import_shared4.IDEMPOTENCY_IN_PROGRESS_CODE && attempt < IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS) {
1884
+ try {
1885
+ this.telemetryHooks?.onRetry?.("session_create", attempt + 1);
1886
+ this.directTelemetry?.log({
1887
+ name: "operation.retry",
1888
+ stage: "session_create",
1889
+ requestCategory: "session_create",
1890
+ attempt: attempt + 1
1891
+ });
1892
+ } catch {
1893
+ }
1055
1894
  await delay(150 * 2 ** attempt);
1056
1895
  continue;
1057
1896
  }
@@ -1074,9 +1913,10 @@ var PaymentAPI = class {
1074
1913
  }
1075
1914
  const uuid = body.data?.uuid;
1076
1915
  if (!uuid) {
1077
- throw new import_shared3.FloPayError("No session ID returned", "api_error");
1916
+ throw new import_shared4.FloPayError("No session ID returned", "api_error");
1078
1917
  }
1079
1918
  this.autoCacheDisplayData(uuid, params);
1919
+ this.adoptDirectTelemetryCheckout(uuid);
1080
1920
  const unifiedSession = await this.getUnifiedCheckoutSession(uuid);
1081
1921
  return {
1082
1922
  ...unifiedSession,
@@ -1086,31 +1926,78 @@ var PaymentAPI = class {
1086
1926
  };
1087
1927
  }
1088
1928
  async waitForCheckoutSessionCompletion(checkoutSessionId, options) {
1929
+ this.beginDirectTelemetryCheckout(checkoutSessionId);
1930
+ const startedAt = this.telemetryTimestamp();
1931
+ this.directTelemetry?.log({
1932
+ name: "operation.recovery.started",
1933
+ stage: "recovery",
1934
+ requestCategory: "session_read",
1935
+ paymentMethodCategory: "saved"
1936
+ });
1089
1937
  const timeoutMs = options?.timeoutMs ?? DEFAULT_PROCESSING_TIMEOUT_MS;
1090
1938
  const deadline = Date.now() + timeoutMs;
1091
1939
  let nextDelayMs = this.clampRetryAfterMs(options?.initialDelayMs ?? DEFAULT_PROCESSING_RETRY_AFTER_MS);
1092
- while (true) {
1093
- const remainingMs = deadline - Date.now();
1094
- if (remainingMs <= 0) {
1095
- throw createCheckoutProcessingTimeoutError();
1096
- }
1097
- if (nextDelayMs > 0) {
1098
- await delay(Math.min(nextDelayMs, remainingMs));
1940
+ let pollAttempt = 0;
1941
+ try {
1942
+ while (true) {
1943
+ const remainingMs = deadline - Date.now();
1944
+ if (remainingMs <= 0) {
1945
+ throw createCheckoutProcessingTimeoutError();
1946
+ }
1947
+ if (nextDelayMs > 0) {
1948
+ try {
1949
+ pollAttempt += 1;
1950
+ this.telemetryHooks?.onRetry?.("session_read", pollAttempt);
1951
+ this.directTelemetry?.log({
1952
+ name: "operation.retry",
1953
+ stage: "recovery",
1954
+ requestCategory: "session_read",
1955
+ paymentMethodCategory: "saved",
1956
+ attempt: pollAttempt
1957
+ });
1958
+ } catch {
1959
+ }
1960
+ await delay(Math.min(nextDelayMs, remainingMs));
1961
+ if (Date.now() >= deadline) {
1962
+ throw createCheckoutProcessingTimeoutError();
1963
+ }
1964
+ }
1965
+ const session = await this.getUnifiedCheckoutSession(checkoutSessionId, options?.nonce);
1966
+ const status = session.data.session?.status;
1967
+ if (status === "complete" || status === "expired") {
1968
+ this.directTelemetry?.log({
1969
+ name: "operation.recovery.completed",
1970
+ stage: "recovery",
1971
+ requestCategory: "session_read",
1972
+ paymentMethodCategory: "saved"
1973
+ });
1974
+ this.directTelemetry?.performance({
1975
+ stage: "recovery",
1976
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1977
+ durationMode: "machine",
1978
+ requestCategory: "session_read",
1979
+ paymentMethodCategory: "saved"
1980
+ });
1981
+ return session;
1982
+ }
1099
1983
  if (Date.now() >= deadline) {
1100
1984
  throw createCheckoutProcessingTimeoutError();
1101
1985
  }
1986
+ nextDelayMs = this.clampRetryAfterMs(
1987
+ Math.max(nextDelayMs * 2, MIN_PROCESSING_RETRY_AFTER_MS)
1988
+ );
1102
1989
  }
1103
- const session = await this.getUnifiedCheckoutSession(checkoutSessionId, options?.nonce);
1104
- const status = session.data.session?.status;
1105
- if (status === "complete" || status === "expired") {
1106
- return session;
1107
- }
1108
- if (Date.now() >= deadline) {
1109
- throw createCheckoutProcessingTimeoutError();
1990
+ } catch (error) {
1991
+ if (error instanceof import_shared4.FloPayError && error.code === "checkout_processing_timeout") {
1992
+ this.reportDirectFailure(
1993
+ error,
1994
+ "RECOVERY_FAILED",
1995
+ "recovery",
1996
+ "session_read",
1997
+ "saved"
1998
+ );
1110
1999
  }
1111
- nextDelayMs = this.clampRetryAfterMs(
1112
- Math.max(nextDelayMs * 2, MIN_PROCESSING_RETRY_AFTER_MS)
1113
- );
2000
+ throw error;
1114
2001
  }
1115
2002
  }
1116
2003
  /** Normalize a raw session into a provider-agnostic shape. */
@@ -1242,7 +2129,7 @@ var PaymentAPI = class {
1242
2129
  return new Response(null, { status: 204, statusText: "No Content" });
1243
2130
  }
1244
2131
  if (session.data.session?.status === "expired") {
1245
- throw new import_shared3.FloPayError(
2132
+ throw new import_shared4.FloPayError(
1246
2133
  "Checkout session has expired.",
1247
2134
  "api_error",
1248
2135
  { code: "checkout_session_expired" }
@@ -1277,12 +2164,12 @@ var PaymentAPI = class {
1277
2164
  */
1278
2165
  autoCacheDisplayData(sessionId, params) {
1279
2166
  if (!sessionId) return;
1280
- const products = params.products ?? (0, import_shared3.foldIntoProducts)(params.items, params.subscriptions);
2167
+ const products = params.products ?? (0, import_shared4.foldIntoProducts)(params.items, params.subscriptions);
1281
2168
  if (products.length === 0 && !params.currency) {
1282
2169
  return;
1283
2170
  }
1284
2171
  const usingUnifiedProducts = params.products !== void 0;
1285
- const sessionCurrency = (0, import_shared3.resolveSessionCurrency)(
2172
+ const sessionCurrency = (0, import_shared4.resolveSessionCurrency)(
1286
2173
  params.currency,
1287
2174
  usingUnifiedProducts ? void 0 : params.items,
1288
2175
  usingUnifiedProducts ? void 0 : params.subscriptions,
@@ -1331,13 +2218,79 @@ var PaymentAPI = class {
1331
2218
  };
1332
2219
  }
1333
2220
  };
2221
+ function createInstrumentedPaymentAPI(billingApiUrl, hooks) {
2222
+ const InstrumentedPaymentAPI = PaymentAPI;
2223
+ return new InstrumentedPaymentAPI(billingApiUrl, hooks);
2224
+ }
1334
2225
 
1335
2226
  // src/pci-vault-card-capture.ts
1336
- var import_shared4 = require("@flopay/shared");
2227
+ var import_shared5 = require("@flopay/shared");
1337
2228
  var VAULT_MESSAGE_SOURCE = "flopay-vault";
1338
- function addBreadcrumb(message, data) {
1339
- const sentry = globalThis.Sentry;
1340
- sentry?.addBreadcrumb?.({ category: "flopay.card-capture", level: "info", message, data });
2229
+ var VAULT_NON_TERMINAL_OUTCOME_LOGS = {
2230
+ ready: ["vault.widget.ready", "vault_ready"],
2231
+ submitting: ["vault.submission.started", "vault_submit"],
2232
+ blocked: ["operation.state_transition", "vault_submit"],
2233
+ action_required: ["vault.action.required", "three_ds_handoff"]
2234
+ };
2235
+ function addBreadcrumb(event) {
2236
+ const data = {
2237
+ class: event.class,
2238
+ stage: event.stage
2239
+ };
2240
+ if ("code" in event) data["code"] = event.code;
2241
+ if ("provider" in event && event.provider) data["provider"] = event.provider;
2242
+ if ("paymentMethodCategory" in event && event.paymentMethodCategory) {
2243
+ data["paymentMethodCategory"] = event.paymentMethodCategory;
2244
+ }
2245
+ if ("outcome" in event) data["outcome"] = event.outcome;
2246
+ if ("durationMs" in event && event.durationMs !== void 0) data["durationMs"] = event.durationMs;
2247
+ if ("durationMode" in event && event.durationMode) data["durationMode"] = event.durationMode;
2248
+ try {
2249
+ const sentry = globalThis.Sentry;
2250
+ sentry?.addBreadcrumb?.({
2251
+ category: "flopay.telemetry",
2252
+ level: event.class === "technical_error" ? "error" : "info",
2253
+ message: event.class === "lifecycle" ? event.name : event.class === "technical_error" ? event.code : event.class === "expected_outcome" ? event.outcome : "sdk.performance",
2254
+ data
2255
+ });
2256
+ } catch {
2257
+ }
2258
+ }
2259
+ function vaultLog(name, stage) {
2260
+ return (0, import_shared5.buildTelemetryLogEvent)({
2261
+ eventId: "11111111-1111-4111-8111-111111111111",
2262
+ name,
2263
+ stage,
2264
+ sequence: 0,
2265
+ provider: "pcivault",
2266
+ paymentMethodCategory: "card"
2267
+ });
2268
+ }
2269
+ function vaultErrorClassification(submissionStarted) {
2270
+ return submissionStarted ? { errorCode: "VAULT_SUBMIT_FAILED", stage: "vault_submit" } : { errorCode: "VAULT_LOAD_FAILED", stage: "vault_mount" };
2271
+ }
2272
+ function vaultOutcomeBreadcrumb(type, submissionStarted) {
2273
+ if (type === "complete" || type === "decline") {
2274
+ return (0, import_shared5.buildTelemetryTerminalEvent)({
2275
+ eventId: "22222222-2222-4222-8222-222222222222",
2276
+ outcome: type === "complete" ? "payment_succeeded" : "payment_declined",
2277
+ sequence: 0,
2278
+ provider: "pcivault",
2279
+ paymentMethodCategory: "card"
2280
+ });
2281
+ }
2282
+ if (type === "error") {
2283
+ const classification = vaultErrorClassification(submissionStarted);
2284
+ return (0, import_shared5.buildTelemetryErrorEvent)({
2285
+ eventId: "33333333-3333-4333-8333-333333333333",
2286
+ ...classification,
2287
+ sequence: 0,
2288
+ provider: "pcivault",
2289
+ paymentMethodCategory: "card"
2290
+ });
2291
+ }
2292
+ const [name, stage] = VAULT_NON_TERMINAL_OUTCOME_LOGS[type];
2293
+ return vaultLog(name, stage);
1341
2294
  }
1342
2295
  function isVaultResultMessage(value) {
1343
2296
  if (typeof value !== "object" || value === null) return false;
@@ -1355,7 +2308,7 @@ function isVaultResizeMessage(value) {
1355
2308
  return record["source"] === VAULT_MESSAGE_SOURCE && record["type"] === "resize" && typeof record["height"] === "number" && Number.isFinite(record["height"]);
1356
2309
  }
1357
2310
  var PciVaultCardCapture = class {
1358
- constructor(config = {}) {
2311
+ constructor(config = {}, internalTelemetry) {
1359
2312
  this.provider = "pcivault";
1360
2313
  this.container = null;
1361
2314
  this.messageHandler = null;
@@ -1387,19 +2340,42 @@ var PciVaultCardCapture = class {
1387
2340
  /** Latest card-field order + autofocus directive to push into the widget. */
1388
2341
  this.cardFieldOrder = null;
1389
2342
  this.cardAutoFocus = true;
2343
+ this.captureRequestedAt = 0;
2344
+ this.vaultReadyReported = false;
2345
+ this.submissionStarted = false;
2346
+ this.submissionStartedAt = null;
1390
2347
  this.listeners = /* @__PURE__ */ new Map();
1391
2348
  this.config = config;
2349
+ this.ownsTelemetryReporter = !internalTelemetry && config.telemetry !== false;
2350
+ this.telemetryReporter = internalTelemetry?.reporter ?? (this.ownsTelemetryReporter ? new TelemetryReporter({
2351
+ billingApiUrl: (0, import_shared5.resolveBillingApiUrl)(),
2352
+ sdkVersion: import_shared5.SDK_VERSION
2353
+ }) : void 0);
2354
+ this.requestedAt = internalTelemetry?.requestedAt;
1392
2355
  }
1393
2356
  async mount(container, options) {
2357
+ if (this.ownsTelemetryReporter && !this.telemetryReporter) {
2358
+ this.telemetryReporter = new TelemetryReporter({
2359
+ billingApiUrl: (0, import_shared5.resolveBillingApiUrl)(),
2360
+ sdkVersion: import_shared5.SDK_VERSION
2361
+ });
2362
+ }
2363
+ const mountedAt = this.telemetryReporter?.now?.() ?? telemetryNow();
2364
+ this.captureRequestedAt = this.requestedAt ?? mountedAt;
2365
+ this.vaultReadyReported = false;
2366
+ this.submissionStarted = false;
2367
+ this.submissionStartedAt = null;
1394
2368
  if (typeof window === "undefined" || typeof document === "undefined") {
1395
- throw new import_shared4.FloPayError(
2369
+ this.reportVaultLoadFailure();
2370
+ throw new import_shared5.FloPayError(
1396
2371
  "The vault card form is only available in the browser.",
1397
2372
  "api_error",
1398
2373
  { code: "card_capture_no_window" }
1399
2374
  );
1400
2375
  }
1401
2376
  if (!options?.html || !options.html.trim()) {
1402
- throw new import_shared4.FloPayError(
2377
+ this.reportVaultLoadFailure();
2378
+ throw new import_shared5.FloPayError(
1403
2379
  "No vault capture widget HTML was provided to mount the secure card form.",
1404
2380
  "api_error",
1405
2381
  { code: "card_capture_no_widget_html" }
@@ -1409,14 +2385,33 @@ var PciVaultCardCapture = class {
1409
2385
  this.messageToken = options.messageToken ?? null;
1410
2386
  this.expectedOrigin = options.expectedOrigin ?? this.config.expectedOrigin ?? null;
1411
2387
  this.theme = options.theme ?? null;
1412
- this.attachMessageListener();
1413
- this.injectWidget(container, options.html);
2388
+ try {
2389
+ this.attachMessageListener();
2390
+ this.injectWidget(container, options.html);
2391
+ } catch (error) {
2392
+ this.reportVaultLoadFailure();
2393
+ throw error;
2394
+ }
1414
2395
  this.postTheme();
1415
2396
  this.postSubmitGate();
1416
2397
  this.postCardFieldOrder();
1417
- addBreadcrumb("vault widget mounted", { sessionId: this.config.sessionId });
2398
+ addBreadcrumb(vaultLog("vault.widget.mounted", "vault_mount"));
2399
+ this.telemetryReporter?.log({
2400
+ name: "vault.widget.mounted",
2401
+ stage: "vault_mount",
2402
+ provider: "pcivault",
2403
+ paymentMethodCategory: "card"
2404
+ });
1418
2405
  this.emit("ready", { sessionId: this.config.sessionId });
1419
2406
  }
2407
+ reportVaultLoadFailure() {
2408
+ this.telemetryReporter?.error({
2409
+ errorCode: "VAULT_LOAD_FAILED",
2410
+ stage: "vault_mount",
2411
+ provider: "pcivault",
2412
+ paymentMethodCategory: "card"
2413
+ });
2414
+ }
1420
2415
  on(event, handler) {
1421
2416
  let set = this.listeners.get(event);
1422
2417
  if (!set) {
@@ -1440,6 +2435,10 @@ var PciVaultCardCapture = class {
1440
2435
  }
1441
2436
  this.messageToken = null;
1442
2437
  this.expectedOrigin = null;
2438
+ if (this.ownsTelemetryReporter) {
2439
+ this.telemetryReporter?.destroy();
2440
+ this.telemetryReporter = void 0;
2441
+ }
1443
2442
  }
1444
2443
  // ── internals ──
1445
2444
  /**
@@ -1493,10 +2492,12 @@ var PciVaultCardCapture = class {
1493
2492
  message: data.message,
1494
2493
  nextActionRedirectUrl: data.nextActionRedirectUrl
1495
2494
  };
1496
- addBreadcrumb(`vault widget ${data.type}`, {
1497
- sessionId: outcome.sessionId,
1498
- declineReason: outcome.declineReason
1499
- });
2495
+ if (data.type === "submitting") {
2496
+ this.submissionStarted = true;
2497
+ this.submissionStartedAt = this.telemetryReporter?.now?.() ?? telemetryNow();
2498
+ }
2499
+ addBreadcrumb(vaultOutcomeBreadcrumb(data.type, this.submissionStarted));
2500
+ this.reportOutcome(data.type);
1500
2501
  if (data.type === "ready") {
1501
2502
  this.postTheme();
1502
2503
  this.postSubmitGate();
@@ -1513,6 +2514,88 @@ var PciVaultCardCapture = class {
1513
2514
  this.messageHandler = handler;
1514
2515
  window.addEventListener("message", handler);
1515
2516
  }
2517
+ reportOutcome(type) {
2518
+ const reporter = this.telemetryReporter;
2519
+ if (!reporter) return;
2520
+ if (type === "ready" && !this.vaultReadyReported) {
2521
+ this.vaultReadyReported = true;
2522
+ reporter.performance({
2523
+ stage: "vault_ready",
2524
+ durationMs: Math.max(0, reporter.now() - this.captureRequestedAt),
2525
+ durationMode: "machine",
2526
+ provider: "pcivault",
2527
+ paymentMethodCategory: "card"
2528
+ });
2529
+ }
2530
+ if (type === "complete" || type === "decline") {
2531
+ reporter.log({
2532
+ name: "vault.terminal",
2533
+ stage: "completion",
2534
+ provider: "pcivault",
2535
+ paymentMethodCategory: "card"
2536
+ });
2537
+ reporter.terminal({
2538
+ outcome: type === "complete" ? "payment_succeeded" : "payment_declined",
2539
+ provider: "pcivault",
2540
+ paymentMethodCategory: "card"
2541
+ });
2542
+ return;
2543
+ }
2544
+ if (type === "error") {
2545
+ const classification = vaultErrorClassification(this.submissionStarted);
2546
+ reporter.log({
2547
+ name: "vault.terminal",
2548
+ stage: classification.stage,
2549
+ provider: "pcivault",
2550
+ paymentMethodCategory: "card"
2551
+ });
2552
+ reporter.error({
2553
+ ...classification,
2554
+ provider: "pcivault",
2555
+ paymentMethodCategory: "card"
2556
+ });
2557
+ return;
2558
+ }
2559
+ if (type === "action_required") {
2560
+ reporter.log({
2561
+ name: "vault.action.required",
2562
+ stage: "three_ds_handoff",
2563
+ provider: "pcivault",
2564
+ paymentMethodCategory: "card"
2565
+ });
2566
+ reporter.log({
2567
+ name: "vault.three_ds.handoff",
2568
+ stage: "three_ds_handoff",
2569
+ provider: "pcivault",
2570
+ paymentMethodCategory: "card"
2571
+ });
2572
+ const submissionStartedAt = this.submissionStartedAt;
2573
+ this.submissionStartedAt = null;
2574
+ if (submissionStartedAt !== null) {
2575
+ reporter.performance({
2576
+ stage: "three_ds_handoff",
2577
+ durationMs: Math.max(0, reporter.now() - submissionStartedAt),
2578
+ durationMode: "machine",
2579
+ provider: "pcivault",
2580
+ paymentMethodCategory: "card"
2581
+ });
2582
+ }
2583
+ reporter.terminal({
2584
+ outcome: "action_required",
2585
+ stage: "three_ds_handoff",
2586
+ provider: "pcivault",
2587
+ paymentMethodCategory: "card"
2588
+ });
2589
+ return;
2590
+ }
2591
+ const [name, stage] = VAULT_NON_TERMINAL_OUTCOME_LOGS[type];
2592
+ reporter.log({
2593
+ name,
2594
+ stage,
2595
+ provider: "pcivault",
2596
+ paymentMethodCategory: "card"
2597
+ });
2598
+ }
1516
2599
  /**
1517
2600
  * Push merchant theme colors into the hosted widget (live). The host calls
1518
2601
  * this on a runtime theme switch; the widget applies them to its CSS variables
@@ -1640,12 +2723,43 @@ var PciVaultCardCapture = class {
1640
2723
  ].join(";");
1641
2724
  frame.src = challengeUrl;
1642
2725
  backdrop.appendChild(frame);
2726
+ const closeButton = document.createElement("button");
2727
+ closeButton.type = "button";
2728
+ closeButton.setAttribute("aria-label", "Close card authentication");
2729
+ closeButton.textContent = "\xD7";
2730
+ closeButton.style.cssText = [
2731
+ "position:fixed",
2732
+ "top:20px",
2733
+ "right:20px",
2734
+ "width:40px",
2735
+ "height:40px",
2736
+ "border:0",
2737
+ "border-radius:9999px",
2738
+ "background:#fff",
2739
+ "color:#0f172a",
2740
+ "font-size:28px",
2741
+ "line-height:40px",
2742
+ "cursor:pointer",
2743
+ "box-shadow:0 4px 14px rgba(0,0,0,0.25)"
2744
+ ].join(";");
2745
+ closeButton.addEventListener("click", () => this.abandonActionRequiredOverlay());
2746
+ backdrop.appendChild(closeButton);
2747
+ backdrop.addEventListener("click", (event) => {
2748
+ if (event.target === backdrop) this.abandonActionRequiredOverlay();
2749
+ });
1643
2750
  const returnHandler = (event) => {
1644
2751
  if (event.source !== frame.contentWindow) return;
1645
2752
  const data = event.data;
1646
2753
  if (!data || typeof data !== "object") return;
1647
2754
  const record = data;
1648
2755
  if (record["source"] !== "flopay-vault-3ds-return") return;
2756
+ addBreadcrumb(vaultLog("vault.three_ds.returned", "three_ds_return"));
2757
+ this.telemetryReporter?.log({
2758
+ name: "vault.three_ds.returned",
2759
+ stage: "three_ds_return",
2760
+ provider: "pcivault",
2761
+ paymentMethodCategory: "card"
2762
+ });
1649
2763
  this.hideActionRequiredOverlay();
1650
2764
  this.postActionCompleted(record["status"]);
1651
2765
  };
@@ -1653,7 +2767,7 @@ var PciVaultCardCapture = class {
1653
2767
  this.threeDsReturnHandler = returnHandler;
1654
2768
  document.body.appendChild(backdrop);
1655
2769
  this.actionOverlay = backdrop;
1656
- addBreadcrumb("vault 3ds challenge overlay shown");
2770
+ addBreadcrumb(vaultLog("vault.three_ds.handoff", "three_ds_handoff"));
1657
2771
  }
1658
2772
  /**
1659
2773
  * Tell the vault widget that the buyer has completed (or abandoned) the
@@ -1679,6 +2793,26 @@ var PciVaultCardCapture = class {
1679
2793
  } catch {
1680
2794
  }
1681
2795
  }
2796
+ abandonActionRequiredOverlay() {
2797
+ if (!this.actionOverlay) return;
2798
+ const breadcrumb = (0, import_shared5.buildTelemetryTerminalEvent)({
2799
+ eventId: "77777777-7777-4777-8777-777777777777",
2800
+ outcome: "customer_abandoned",
2801
+ stage: "three_ds_handoff",
2802
+ sequence: 0,
2803
+ provider: "pcivault",
2804
+ paymentMethodCategory: "card"
2805
+ });
2806
+ addBreadcrumb(breadcrumb);
2807
+ this.telemetryReporter?.terminal({
2808
+ outcome: "customer_abandoned",
2809
+ stage: "three_ds_handoff",
2810
+ provider: "pcivault",
2811
+ paymentMethodCategory: "card"
2812
+ });
2813
+ this.hideActionRequiredOverlay();
2814
+ this.postActionCompleted("abandoned");
2815
+ }
1682
2816
  hideActionRequiredOverlay() {
1683
2817
  if (this.threeDsReturnHandler) {
1684
2818
  window.removeEventListener("message", this.threeDsReturnHandler);
@@ -1687,7 +2821,6 @@ var PciVaultCardCapture = class {
1687
2821
  if (!this.actionOverlay) return;
1688
2822
  this.actionOverlay.parentNode?.removeChild(this.actionOverlay);
1689
2823
  this.actionOverlay = null;
1690
- addBreadcrumb("vault 3ds challenge overlay hidden");
1691
2824
  }
1692
2825
  /**
1693
2826
  * Size the hosted-widget iframe to the height reported by the form inside it.
@@ -1703,13 +2836,64 @@ var PciVaultCardCapture = class {
1703
2836
  iframe.style.height = `${clamped}px`;
1704
2837
  }
1705
2838
  };
2839
+ function createInstrumentedPciVaultCardCapture(config, reporter, requestedAt) {
2840
+ const InstrumentedCapture = PciVaultCardCapture;
2841
+ return new InstrumentedCapture(config, { reporter, requestedAt });
2842
+ }
2843
+
2844
+ // src/telemetry-bridge.ts
2845
+ var FLOPAY_TELEMETRY_BRIDGE = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.bridge.v1");
2846
+ function attachFloPayTelemetryBridge(target, reporter, fallbackNow) {
2847
+ const now = () => reporter?.now() ?? fallbackNow();
2848
+ const bridge = {
2849
+ error: (input) => reporter?.error(input),
2850
+ log: (input) => reporter?.log(input),
2851
+ performance: (input) => reporter?.performance(input),
2852
+ terminal: (input) => reporter?.terminal(input),
2853
+ now,
2854
+ elapsed: (startedAt) => Math.max(0, now() - startedAt),
2855
+ setCheckoutContext: (context) => reporter?.setCheckoutContext(context),
2856
+ beginCheckout: (context = {}) => reporter?.beginCheckout(context) ?? now(),
2857
+ disable: () => reporter?.disable()
2858
+ };
2859
+ Object.defineProperty(target, FLOPAY_TELEMETRY_BRIDGE, {
2860
+ configurable: false,
2861
+ enumerable: false,
2862
+ writable: false,
2863
+ value: bridge
2864
+ });
2865
+ }
2866
+ function getFloPayTelemetryBridge(target) {
2867
+ return target[FLOPAY_TELEMETRY_BRIDGE];
2868
+ }
1706
2869
 
1707
2870
  // src/flopay.ts
2871
+ function isExpectedDecline(error) {
2872
+ if (!error) return false;
2873
+ const code = error.code?.toLowerCase() ?? "";
2874
+ return Boolean(error.declineCode) || code.includes("declin");
2875
+ }
2876
+ function telemetryProvider(name) {
2877
+ if (name === "stripe" || name === "paypal" || name === "pcivault") return name;
2878
+ return "other";
2879
+ }
2880
+ var CARD_THREE_DS_ATTEMPT_TTL_MS2 = 15 * 6e4;
2881
+ var MAX_CARD_THREE_DS_ATTEMPTS2 = 32;
1708
2882
  var FloPay = class {
1709
- constructor(provider, config) {
2883
+ constructor(provider, config, telemetryReporter) {
1710
2884
  this.currentElements = null;
2885
+ this.cardThreeDsStartedAt = /* @__PURE__ */ new Map();
1711
2886
  this.provider = provider;
1712
2887
  this.config = config;
2888
+ this.telemetryReporter = telemetryReporter ?? new TelemetryReporter({
2889
+ billingApiUrl: (0, import_shared6.resolveBillingApiUrl)(config.billingApiUrl),
2890
+ sdkVersion: import_shared6.SDK_VERSION,
2891
+ enabled: config.telemetry !== false
2892
+ });
2893
+ attachFloPayTelemetryBridge(this, this.telemetryReporter, telemetryNow);
2894
+ }
2895
+ now() {
2896
+ return this.telemetryReporter?.now?.() ?? telemetryNow();
1713
2897
  }
1714
2898
  /**
1715
2899
  * Creates a new `FloPayElements` group for mounting payment fields.
@@ -1733,11 +2917,169 @@ var FloPay = class {
1733
2917
  }
1734
2918
  /** Create a payment method from the current elements (tokenize card). */
1735
2919
  async createPaymentMethod(billingDetails) {
1736
- return this.provider.createPaymentMethod(billingDetails);
2920
+ const started = this.now();
2921
+ const provider = telemetryProvider(this.provider.name);
2922
+ this.telemetryReporter?.log({
2923
+ name: "payment.tokenization.started",
2924
+ stage: "tokenization",
2925
+ provider,
2926
+ paymentMethodCategory: "card"
2927
+ });
2928
+ try {
2929
+ const result = await this.provider.createPaymentMethod(billingDetails);
2930
+ this.telemetryReporter?.performance({
2931
+ stage: "tokenization",
2932
+ durationMs: this.now() - started,
2933
+ durationMode: "machine",
2934
+ provider,
2935
+ paymentMethodCategory: "card"
2936
+ });
2937
+ if (!result.error) {
2938
+ this.telemetryReporter?.log({
2939
+ name: "payment.tokenization.completed",
2940
+ stage: "tokenization",
2941
+ provider,
2942
+ paymentMethodCategory: "card"
2943
+ });
2944
+ } else if (result.error.type !== "validation_error") {
2945
+ this.telemetryReporter?.error({
2946
+ errorCode: "TOKENIZATION_FAILED",
2947
+ stage: "tokenization",
2948
+ provider,
2949
+ paymentMethodCategory: "card"
2950
+ });
2951
+ }
2952
+ return result;
2953
+ } catch (error) {
2954
+ this.telemetryReporter?.performance({
2955
+ stage: "tokenization",
2956
+ durationMs: this.now() - started,
2957
+ durationMode: "machine",
2958
+ provider,
2959
+ paymentMethodCategory: "card"
2960
+ });
2961
+ this.telemetryReporter?.error({
2962
+ errorCode: "TOKENIZATION_FAILED",
2963
+ stage: "tokenization",
2964
+ provider,
2965
+ paymentMethodCategory: "card"
2966
+ });
2967
+ throw error;
2968
+ }
1737
2969
  }
1738
2970
  /** Confirm a card payment with a known client secret and payment method ID. */
1739
2971
  async confirmCardPayment(params) {
1740
- return this.provider.confirmCardPayment(params);
2972
+ const provider = telemetryProvider(this.provider.name);
2973
+ const operationStartedAt = this.now();
2974
+ try {
2975
+ const result = await this.provider.confirmCardPayment(params);
2976
+ const completedAt = this.now();
2977
+ const threeDs = result.threeDs;
2978
+ let threeDsFailed = false;
2979
+ if (threeDs?.status === "handoff") {
2980
+ if (this.trackCardThreeDsAttempt(threeDs.attemptId, completedAt)) {
2981
+ this.telemetryReporter?.log({
2982
+ name: "payment.three_ds.handoff",
2983
+ stage: "three_ds_handoff",
2984
+ provider,
2985
+ paymentMethodCategory: "card"
2986
+ });
2987
+ this.telemetryReporter?.performance({
2988
+ stage: "three_ds_handoff",
2989
+ durationMs: completedAt - operationStartedAt,
2990
+ durationMode: "machine",
2991
+ provider,
2992
+ paymentMethodCategory: "card"
2993
+ });
2994
+ this.telemetryReporter?.terminal({
2995
+ outcome: "action_required",
2996
+ stage: "three_ds_handoff",
2997
+ provider,
2998
+ paymentMethodCategory: "card"
2999
+ });
3000
+ }
3001
+ } else if (threeDs) {
3002
+ const threeDsStartedAt = this.takeCardThreeDsAttempt(threeDs.attemptId, completedAt);
3003
+ if (threeDsStartedAt !== void 0) {
3004
+ if (threeDs.status === "returned") {
3005
+ this.telemetryReporter?.log({
3006
+ name: "payment.three_ds.returned",
3007
+ stage: "three_ds_return",
3008
+ provider,
3009
+ paymentMethodCategory: "card"
3010
+ });
3011
+ this.telemetryReporter?.performance({
3012
+ stage: "three_ds_return",
3013
+ durationMs: completedAt - threeDsStartedAt,
3014
+ durationMode: "machine",
3015
+ provider,
3016
+ paymentMethodCategory: "card"
3017
+ });
3018
+ } else {
3019
+ threeDsFailed = true;
3020
+ }
3021
+ }
3022
+ }
3023
+ if (isExpectedDecline(result.error)) {
3024
+ this.telemetryReporter?.terminal({
3025
+ outcome: "payment_declined",
3026
+ provider,
3027
+ paymentMethodCategory: "card"
3028
+ });
3029
+ } else if (result.error?.type === "validation_error") {
3030
+ this.telemetryReporter?.terminal({
3031
+ outcome: "validation_rejected",
3032
+ provider,
3033
+ paymentMethodCategory: "card"
3034
+ });
3035
+ } else if (result.error) {
3036
+ this.telemetryReporter?.error({
3037
+ errorCode: threeDsFailed ? "THREE_DS_FAILED" : "PROVIDER_RUNTIME_FAILED",
3038
+ stage: threeDsFailed ? "three_ds_return" : "processing",
3039
+ provider,
3040
+ paymentMethodCategory: "card"
3041
+ });
3042
+ } else if (!threeDsFailed && threeDs?.status !== "handoff" && result.status === "succeeded") {
3043
+ this.telemetryReporter?.terminal({
3044
+ outcome: "payment_succeeded",
3045
+ provider,
3046
+ paymentMethodCategory: "card"
3047
+ });
3048
+ }
3049
+ return result;
3050
+ } catch (error) {
3051
+ this.telemetryReporter?.error({
3052
+ errorCode: "PROVIDER_RUNTIME_FAILED",
3053
+ stage: "processing",
3054
+ provider,
3055
+ paymentMethodCategory: "card"
3056
+ });
3057
+ throw error;
3058
+ }
3059
+ }
3060
+ trackCardThreeDsAttempt(attemptId, startedAt) {
3061
+ this.pruneExpiredCardThreeDsAttempts(startedAt);
3062
+ if (this.cardThreeDsStartedAt.has(attemptId)) return false;
3063
+ while (this.cardThreeDsStartedAt.size >= MAX_CARD_THREE_DS_ATTEMPTS2) {
3064
+ const oldestAttemptId = this.cardThreeDsStartedAt.keys().next().value;
3065
+ if (oldestAttemptId === void 0) break;
3066
+ this.cardThreeDsStartedAt.delete(oldestAttemptId);
3067
+ }
3068
+ this.cardThreeDsStartedAt.set(attemptId, startedAt);
3069
+ return true;
3070
+ }
3071
+ takeCardThreeDsAttempt(attemptId, now) {
3072
+ this.pruneExpiredCardThreeDsAttempts(now);
3073
+ const startedAt = this.cardThreeDsStartedAt.get(attemptId);
3074
+ if (startedAt !== void 0) this.cardThreeDsStartedAt.delete(attemptId);
3075
+ return startedAt;
3076
+ }
3077
+ pruneExpiredCardThreeDsAttempts(now) {
3078
+ for (const [attemptId, startedAt] of this.cardThreeDsStartedAt) {
3079
+ if (now - startedAt >= CARD_THREE_DS_ATTEMPT_TTL_MS2) {
3080
+ this.cardThreeDsStartedAt.delete(attemptId);
3081
+ }
3082
+ }
1741
3083
  }
1742
3084
  /**
1743
3085
  * Create a {@link CardCaptureAdapter} for collecting card details through the
@@ -1752,21 +3094,270 @@ var FloPay = class {
1752
3094
  * the SDK runtime.
1753
3095
  */
1754
3096
  cardCapture(options) {
3097
+ const requestedAt = this.now();
3098
+ this.telemetryReporter?.log({
3099
+ name: "vault.capture.requested",
3100
+ stage: "vault_request",
3101
+ provider: "pcivault",
3102
+ paymentMethodCategory: "card"
3103
+ });
3104
+ if (this.telemetryReporter) {
3105
+ return createInstrumentedPciVaultCardCapture(
3106
+ { sessionId: options?.sessionId },
3107
+ this.telemetryReporter,
3108
+ requestedAt
3109
+ );
3110
+ }
1755
3111
  return new PciVaultCardCapture({
1756
- sessionId: options?.sessionId
3112
+ sessionId: options?.sessionId,
3113
+ telemetry: false
1757
3114
  });
1758
3115
  }
1759
3116
  /** Confirm a PayPal payment: create intent via billing API → confirm → redirect if needed. */
1760
3117
  async confirmPayPalPayment(params) {
1761
- return this.provider.confirmPayPalPayment(params);
3118
+ const startedAt = this.now();
3119
+ this.telemetryReporter?.log({
3120
+ name: "payment.method.selected",
3121
+ stage: "processing",
3122
+ provider: "paypal",
3123
+ paymentMethodCategory: "paypal"
3124
+ });
3125
+ this.telemetryReporter?.log({
3126
+ name: "payment.intent.started",
3127
+ stage: "processing",
3128
+ provider: "paypal",
3129
+ paymentMethodCategory: "paypal",
3130
+ requestCategory: "intent_create"
3131
+ });
3132
+ try {
3133
+ const result = await this.provider.confirmPayPalPayment(params);
3134
+ this.telemetryReporter?.performance({
3135
+ stage: "processing",
3136
+ durationMs: this.now() - startedAt,
3137
+ durationMode: "machine",
3138
+ provider: "paypal",
3139
+ paymentMethodCategory: "paypal"
3140
+ });
3141
+ if (!result.error) {
3142
+ this.telemetryReporter?.log({
3143
+ name: "payment.intent.completed",
3144
+ stage: "processing",
3145
+ provider: "paypal",
3146
+ paymentMethodCategory: "paypal",
3147
+ requestCategory: "intent_create",
3148
+ statusClass: "2xx"
3149
+ });
3150
+ }
3151
+ if (result.status === "requires_action") {
3152
+ this.telemetryReporter?.log({
3153
+ name: "provider.redirect.started",
3154
+ stage: "redirect",
3155
+ provider: "paypal",
3156
+ paymentMethodCategory: "paypal"
3157
+ });
3158
+ this.telemetryReporter?.terminal({
3159
+ outcome: "action_required",
3160
+ stage: "redirect",
3161
+ provider: "paypal",
3162
+ paymentMethodCategory: "paypal"
3163
+ });
3164
+ } else if (result.status === "succeeded") {
3165
+ this.telemetryReporter?.terminal({
3166
+ outcome: "payment_succeeded",
3167
+ provider: "paypal",
3168
+ paymentMethodCategory: "paypal"
3169
+ });
3170
+ } else if (result.status !== "processing") {
3171
+ if (isExpectedDecline(result.error)) {
3172
+ this.telemetryReporter?.terminal({
3173
+ outcome: "payment_declined",
3174
+ provider: "paypal",
3175
+ paymentMethodCategory: "paypal"
3176
+ });
3177
+ } else if (result.error?.type === "validation_error") {
3178
+ this.telemetryReporter?.terminal({
3179
+ outcome: "validation_rejected",
3180
+ provider: "paypal",
3181
+ paymentMethodCategory: "paypal"
3182
+ });
3183
+ } else if (result.error) {
3184
+ this.telemetryReporter?.error({
3185
+ errorCode: "PAYMENT_PROCESSING_FAILED",
3186
+ stage: "processing",
3187
+ provider: "paypal",
3188
+ paymentMethodCategory: "paypal",
3189
+ requestCategory: "intent_create"
3190
+ });
3191
+ }
3192
+ }
3193
+ return result;
3194
+ } catch (error) {
3195
+ this.telemetryReporter?.performance({
3196
+ stage: "processing",
3197
+ durationMs: this.now() - startedAt,
3198
+ durationMode: "machine",
3199
+ provider: "paypal",
3200
+ paymentMethodCategory: "paypal"
3201
+ });
3202
+ this.telemetryReporter?.error({
3203
+ errorCode: "NETWORK_REQUEST_FAILED",
3204
+ stage: "processing",
3205
+ provider: "paypal",
3206
+ paymentMethodCategory: "paypal",
3207
+ requestCategory: "intent_create",
3208
+ statusClass: "network_error"
3209
+ });
3210
+ throw error;
3211
+ }
1762
3212
  }
1763
3213
  /** Resume a PayPal payment after redirect return. Returns null if no PayPal params in URL. */
1764
3214
  async resumePayPalPayment() {
1765
- return this.provider.resumePayPalPayment();
3215
+ const startedAt = this.now();
3216
+ try {
3217
+ const result = await this.provider.resumePayPalPayment();
3218
+ if (result === null) return null;
3219
+ this.telemetryReporter?.log({
3220
+ name: "provider.redirect.resumed",
3221
+ stage: "redirect_resume",
3222
+ provider: "paypal",
3223
+ paymentMethodCategory: "paypal"
3224
+ });
3225
+ this.telemetryReporter?.performance({
3226
+ stage: "redirect_resume",
3227
+ durationMs: this.now() - startedAt,
3228
+ durationMode: "machine",
3229
+ provider: "paypal",
3230
+ paymentMethodCategory: "paypal"
3231
+ });
3232
+ if (result.status === "succeeded") {
3233
+ this.telemetryReporter?.terminal({
3234
+ outcome: "payment_succeeded",
3235
+ provider: "paypal",
3236
+ paymentMethodCategory: "paypal"
3237
+ });
3238
+ } else if (isExpectedDecline(result.error)) {
3239
+ this.telemetryReporter?.terminal({
3240
+ outcome: "payment_declined",
3241
+ provider: "paypal",
3242
+ paymentMethodCategory: "paypal"
3243
+ });
3244
+ } else if (result.error?.type === "validation_error") {
3245
+ this.telemetryReporter?.terminal({
3246
+ outcome: "validation_rejected",
3247
+ provider: "paypal",
3248
+ paymentMethodCategory: "paypal"
3249
+ });
3250
+ } else if (result.error) {
3251
+ this.telemetryReporter?.error({
3252
+ errorCode: "REDIRECT_RESUME_FAILED",
3253
+ stage: "redirect_resume",
3254
+ provider: "paypal",
3255
+ paymentMethodCategory: "paypal"
3256
+ });
3257
+ }
3258
+ return result;
3259
+ } catch (error) {
3260
+ this.telemetryReporter?.performance({
3261
+ stage: "redirect_resume",
3262
+ durationMs: this.now() - startedAt,
3263
+ durationMode: "machine",
3264
+ provider: "paypal",
3265
+ paymentMethodCategory: "paypal"
3266
+ });
3267
+ this.telemetryReporter?.error({
3268
+ errorCode: "REDIRECT_RESUME_FAILED",
3269
+ stage: "redirect_resume",
3270
+ provider: "paypal",
3271
+ paymentMethodCategory: "paypal"
3272
+ });
3273
+ throw error;
3274
+ }
1766
3275
  }
1767
3276
  /** Confirms a payment using the mounted elements. */
1768
3277
  async confirmPayment(params) {
1769
- return this.provider.confirmPayment(params);
3278
+ const started = this.now();
3279
+ const provider = telemetryProvider(this.provider.name);
3280
+ this.telemetryReporter?.log({
3281
+ name: "payment.processing.started",
3282
+ stage: "processing",
3283
+ provider,
3284
+ paymentMethodCategory: "unknown"
3285
+ });
3286
+ try {
3287
+ const result = await this.provider.confirmPayment(params);
3288
+ const durationMs = this.now() - started;
3289
+ this.telemetryReporter?.performance({
3290
+ stage: "processing",
3291
+ durationMs,
3292
+ durationMode: "machine",
3293
+ provider,
3294
+ paymentMethodCategory: "unknown"
3295
+ });
3296
+ if (result.status === "succeeded") {
3297
+ this.telemetryReporter?.terminal({
3298
+ outcome: "payment_succeeded",
3299
+ provider,
3300
+ paymentMethodCategory: "unknown"
3301
+ });
3302
+ } else if (isExpectedDecline(result.error)) {
3303
+ this.telemetryReporter?.terminal({
3304
+ outcome: "payment_declined",
3305
+ provider,
3306
+ paymentMethodCategory: "unknown"
3307
+ });
3308
+ } else if (result.error?.type === "validation_error") {
3309
+ this.telemetryReporter?.terminal({
3310
+ outcome: "validation_rejected",
3311
+ provider,
3312
+ paymentMethodCategory: "unknown"
3313
+ });
3314
+ } else if (result.status === "requires_action") {
3315
+ this.telemetryReporter?.terminal({
3316
+ outcome: "action_required",
3317
+ stage: "three_ds_handoff",
3318
+ provider,
3319
+ paymentMethodCategory: "unknown"
3320
+ });
3321
+ } else if (result.status === "failed" && result.error) {
3322
+ this.telemetryReporter?.error({
3323
+ errorCode: "PAYMENT_PROCESSING_FAILED",
3324
+ stage: "processing",
3325
+ provider,
3326
+ paymentMethodCategory: "unknown"
3327
+ });
3328
+ }
3329
+ if (result.status === "succeeded" || result.status === "failed") {
3330
+ this.telemetryReporter?.log({
3331
+ name: "payment.processing.completed",
3332
+ stage: "processing",
3333
+ provider,
3334
+ paymentMethodCategory: "unknown"
3335
+ });
3336
+ } else {
3337
+ this.telemetryReporter?.log({
3338
+ name: "operation.state_transition",
3339
+ stage: result.status === "requires_action" ? "three_ds_handoff" : "processing",
3340
+ provider,
3341
+ paymentMethodCategory: "unknown"
3342
+ });
3343
+ }
3344
+ return result;
3345
+ } catch (error) {
3346
+ this.telemetryReporter?.performance({
3347
+ stage: "processing",
3348
+ durationMs: this.now() - started,
3349
+ durationMode: "machine",
3350
+ provider,
3351
+ paymentMethodCategory: "unknown"
3352
+ });
3353
+ this.telemetryReporter?.error({
3354
+ errorCode: "PAYMENT_PROCESSING_FAILED",
3355
+ stage: "processing",
3356
+ provider,
3357
+ paymentMethodCategory: "unknown"
3358
+ });
3359
+ throw error;
3360
+ }
1770
3361
  }
1771
3362
  /**
1772
3363
  * Retrieves a checkout session by ID via the billing API.
@@ -1779,17 +3370,15 @@ var FloPay = class {
1779
3370
  */
1780
3371
  async retrieveSession(sessionId, billingApiUrl) {
1781
3372
  if (!sessionId) {
1782
- throw new import_shared5.FloPayError(
3373
+ throw new import_shared6.FloPayError(
1783
3374
  "sessionId is required to retrieve a session.",
1784
3375
  "validation_error",
1785
3376
  { param: "sessionId" }
1786
3377
  );
1787
3378
  }
1788
- const apiUrl = (0, import_shared5.resolveBillingApiUrl)(billingApiUrl ?? this.config.billingApiUrl);
1789
- const api = new PaymentAPI(apiUrl);
1790
- const unified = await api.getUnifiedCheckoutSession(sessionId);
3379
+ const unified = await this.retrieveUnifiedSession(sessionId, billingApiUrl);
1791
3380
  if (!unified.data.session) {
1792
- throw new import_shared5.FloPayError("Session not found", "api_error");
3381
+ throw new import_shared6.FloPayError("Session not found", "api_error");
1793
3382
  }
1794
3383
  return unified.data.session;
1795
3384
  }
@@ -1802,15 +3391,86 @@ var FloPay = class {
1802
3391
  */
1803
3392
  async retrieveUnifiedSession(sessionId, billingApiUrl) {
1804
3393
  if (!sessionId) {
1805
- throw new import_shared5.FloPayError(
3394
+ throw new import_shared6.FloPayError(
1806
3395
  "sessionId is required.",
1807
3396
  "validation_error",
1808
3397
  { param: "sessionId" }
1809
3398
  );
1810
3399
  }
1811
- const apiUrl = (0, import_shared5.resolveBillingApiUrl)(billingApiUrl ?? this.config.billingApiUrl);
1812
- const api = new PaymentAPI(apiUrl);
1813
- return api.getUnifiedCheckoutSession(sessionId);
3400
+ const apiUrl = (0, import_shared6.resolveBillingApiUrl)(billingApiUrl ?? this.config.billingApiUrl);
3401
+ const started = this.now();
3402
+ this.telemetryReporter?.log({
3403
+ name: "session.read.started",
3404
+ stage: "session_read",
3405
+ requestCategory: "session_read"
3406
+ });
3407
+ let firstByteDuration;
3408
+ const api = createInstrumentedPaymentAPI(apiUrl, {
3409
+ now: () => this.telemetryReporter?.now() ?? telemetryNow(),
3410
+ onFirstByte: (durationMs) => {
3411
+ firstByteDuration = durationMs;
3412
+ },
3413
+ onRetry: (requestCategory, attempt) => {
3414
+ this.telemetryReporter?.log({
3415
+ name: "operation.retry",
3416
+ stage: requestCategory === "session_read" ? "session_read" : "processing",
3417
+ requestCategory,
3418
+ attempt
3419
+ });
3420
+ }
3421
+ });
3422
+ try {
3423
+ const result = await api.getUnifiedCheckoutSession(sessionId);
3424
+ if (firstByteDuration !== void 0) {
3425
+ this.telemetryReporter?.log({
3426
+ name: "session.request.first_byte",
3427
+ stage: "session_first_byte",
3428
+ requestCategory: "session_read",
3429
+ statusClass: "2xx"
3430
+ });
3431
+ this.telemetryReporter?.performance({
3432
+ stage: "session_first_byte",
3433
+ durationMs: firstByteDuration,
3434
+ durationMode: "machine",
3435
+ requestCategory: "session_read",
3436
+ statusClass: "2xx"
3437
+ });
3438
+ }
3439
+ this.telemetryReporter?.log({
3440
+ name: "session.request.completed",
3441
+ stage: "session_complete",
3442
+ requestCategory: "session_read",
3443
+ statusClass: "2xx"
3444
+ });
3445
+ this.telemetryReporter?.log({
3446
+ name: "checkout.data.ready",
3447
+ stage: "checkout_data_ready"
3448
+ });
3449
+ this.telemetryReporter?.performance({
3450
+ stage: "session_complete",
3451
+ durationMs: this.now() - started,
3452
+ durationMode: "machine",
3453
+ requestCategory: "session_read",
3454
+ statusClass: "2xx"
3455
+ });
3456
+ return result;
3457
+ } catch (error) {
3458
+ const statusCode = error instanceof import_shared6.FloPayError ? error.statusCode : void 0;
3459
+ this.telemetryReporter?.error({
3460
+ errorCode: error instanceof import_shared6.FloPayError && error.code === "checkout_processing_timeout" ? "REQUEST_TIMEOUT" : "NETWORK_REQUEST_FAILED",
3461
+ stage: "session_read",
3462
+ provider: "flo",
3463
+ paymentMethodCategory: "unknown"
3464
+ });
3465
+ this.telemetryReporter?.performance({
3466
+ stage: "session_complete",
3467
+ durationMs: this.now() - started,
3468
+ durationMode: "machine",
3469
+ requestCategory: "session_read",
3470
+ statusClass: statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
3471
+ });
3472
+ throw error;
3473
+ }
1814
3474
  }
1815
3475
  /**
1816
3476
  * Returns the raw underlying provider instance (e.g. Stripe object).
@@ -1822,37 +3482,128 @@ var FloPay = class {
1822
3482
  }
1823
3483
  /** Tears down the SDK instance and releases resources. */
1824
3484
  destroy() {
3485
+ this.telemetryReporter?.log({ name: "checkout.unmount", stage: "unmount" });
3486
+ this.telemetryReporter?.destroy();
3487
+ this.cardThreeDsStartedAt.clear();
1825
3488
  this.currentElements?.destroy();
1826
3489
  this.currentElements = null;
1827
3490
  this.provider.destroy();
1828
3491
  }
1829
3492
  };
3493
+ function createInstrumentedFloPay(provider, config, reporter) {
3494
+ const InstrumentedFloPay = FloPay;
3495
+ return new InstrumentedFloPay(provider, config, reporter);
3496
+ }
1830
3497
 
1831
3498
  // src/load.ts
1832
3499
  var instanceCache = /* @__PURE__ */ new Map();
3500
+ function stableCacheValue(value) {
3501
+ if (Array.isArray(value)) return value.map(stableCacheValue);
3502
+ if (value && typeof value === "object") {
3503
+ return Object.fromEntries(
3504
+ Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, stableCacheValue(entry)])
3505
+ );
3506
+ }
3507
+ return value;
3508
+ }
3509
+ function instanceCacheKey(publishableKey, options) {
3510
+ return JSON.stringify([
3511
+ publishableKey,
3512
+ (0, import_shared7.resolveBillingApiUrl)(options?.billingApiUrl),
3513
+ options?.telemetry !== false,
3514
+ options?.locale ?? "auto",
3515
+ options?.apiVersion ?? null,
3516
+ stableCacheValue(options?.appearance ?? null)
3517
+ ]);
3518
+ }
1833
3519
  async function loadFloPay(publishableKey, options) {
1834
3520
  if (!publishableKey) {
1835
- throw new import_shared6.FloPayError(
3521
+ const reporter2 = new TelemetryReporter({
3522
+ billingApiUrl: (0, import_shared7.resolveBillingApiUrl)(options?.billingApiUrl),
3523
+ sdkVersion: import_shared7.SDK_VERSION,
3524
+ enabled: options?.telemetry !== false
3525
+ });
3526
+ reporter2.error({
3527
+ errorCode: "CONFIGURATION_INVALID",
3528
+ stage: "sdk_initialize",
3529
+ paymentMethodCategory: "unknown"
3530
+ });
3531
+ void reporter2.flush().catch(() => {
3532
+ }).finally(() => reporter2.destroy());
3533
+ throw new import_shared7.FloPayError(
1836
3534
  "A publishable key is required to initialize FloPay.",
1837
3535
  "validation_error",
1838
3536
  { param: "publishableKey" }
1839
3537
  );
1840
3538
  }
1841
- const cached = instanceCache.get(publishableKey);
1842
- if (cached) return cached;
3539
+ const cacheKey = instanceCacheKey(publishableKey, options);
3540
+ const cached = instanceCache.get(cacheKey);
3541
+ if (cached) {
3542
+ if (options?.telemetry !== false) {
3543
+ getFloPayTelemetryBridge(cached)?.log({
3544
+ name: "sdk.cache.hit",
3545
+ stage: "sdk_initialize"
3546
+ });
3547
+ }
3548
+ return cached;
3549
+ }
1843
3550
  const config = {
1844
3551
  publishableKey,
1845
3552
  ...options
1846
3553
  };
3554
+ const reporter = new TelemetryReporter({
3555
+ billingApiUrl: (0, import_shared7.resolveBillingApiUrl)(config.billingApiUrl),
3556
+ sdkVersion: import_shared7.SDK_VERSION,
3557
+ enabled: config.telemetry !== false
3558
+ });
3559
+ const initializationStarted = reporter.now();
3560
+ reporter.log({ name: "sdk.initialize.started", stage: "sdk_initialize" });
3561
+ reporter.log({ name: "sdk.cache.miss", stage: "sdk_initialize" });
3562
+ reporter.log({
3563
+ name: "provider.load.started",
3564
+ stage: "provider_load",
3565
+ provider: "stripe"
3566
+ });
1847
3567
  const adapter = new StripeAdapter();
1848
- await adapter.initialize(config);
1849
- const instance = new FloPay(adapter, config);
1850
- instanceCache.set(publishableKey, instance);
3568
+ try {
3569
+ await adapter.initialize(config);
3570
+ } catch (error) {
3571
+ reporter.error({
3572
+ errorCode: "SDK_INITIALIZATION_FAILED",
3573
+ stage: "sdk_initialize",
3574
+ provider: "stripe",
3575
+ paymentMethodCategory: "unknown"
3576
+ });
3577
+ reporter.destroy();
3578
+ throw error;
3579
+ }
3580
+ reporter.log({ name: "provider.ready", stage: "provider_ready", provider: "stripe" });
3581
+ reporter.log({
3582
+ name: "provider.availability.checked",
3583
+ stage: "provider_ready",
3584
+ provider: "stripe"
3585
+ });
3586
+ reporter.log({ name: "sdk.initialize.ready", stage: "sdk_initialize" });
3587
+ const initializationDuration = reporter.now() - initializationStarted;
3588
+ reporter.performance({
3589
+ stage: "sdk_initialize",
3590
+ durationMs: initializationDuration,
3591
+ durationMode: "machine",
3592
+ provider: "stripe"
3593
+ });
3594
+ reporter.performance({
3595
+ stage: "provider_ready",
3596
+ durationMs: initializationDuration,
3597
+ durationMode: "machine",
3598
+ provider: "stripe"
3599
+ });
3600
+ const instance = createInstrumentedFloPay(adapter, config, reporter);
3601
+ instanceCache.set(cacheKey, instance);
1851
3602
  return instance;
1852
3603
  }
1853
3604
 
1854
3605
  // src/create-checkout-session.ts
1855
- var import_shared7 = require("@flopay/shared");
3606
+ var import_shared8 = require("@flopay/shared");
1856
3607
  var MAX_COUPON_CODES = 5;
1857
3608
  function readString2(value) {
1858
3609
  return typeof value === "string" && value.trim() ? value : void 0;
@@ -1861,7 +3612,7 @@ function buildCheckoutSessionError(status, payload) {
1861
3612
  const nested = payload?.error;
1862
3613
  const code = readString2(payload?.code) ?? readString2(nested?.code) ?? `http_${status}`;
1863
3614
  const message = readString2(payload?.message) ?? readString2(nested?.message) ?? defaultMessageForCode(code, status);
1864
- return new import_shared7.FloPayError(message, "api_error", { code, statusCode: status });
3615
+ return new import_shared8.FloPayError(message, "api_error", { code, statusCode: status });
1865
3616
  }
1866
3617
  function defaultMessageForCode(code, status) {
1867
3618
  switch (code) {
@@ -1873,7 +3624,7 @@ function defaultMessageForCode(code, status) {
1873
3624
  return `Failed to create checkout session (HTTP ${status}).`;
1874
3625
  }
1875
3626
  }
1876
- async function createCheckoutSession(options) {
3627
+ async function createCheckoutSessionCore(options, onFirstByte) {
1877
3628
  const {
1878
3629
  billingApiUrl,
1879
3630
  checkoutBaseUrl,
@@ -1894,18 +3645,18 @@ async function createCheckoutSession(options) {
1894
3645
  utmMetadata,
1895
3646
  idempotencyKey
1896
3647
  } = options;
1897
- const resolvedIdempotencyKey = (0, import_shared7.resolveIdempotencyKey)(idempotencyKey);
3648
+ const resolvedIdempotencyKey = (0, import_shared8.resolveIdempotencyKey)(idempotencyKey);
1898
3649
  if (couponCodes.length > MAX_COUPON_CODES) {
1899
- throw new import_shared7.FloPayError(
3650
+ throw new import_shared8.FloPayError(
1900
3651
  `Too many coupon codes \u2014 a checkout session accepts at most ${MAX_COUPON_CODES}.`,
1901
3652
  "validation_error",
1902
3653
  { code: "CouponLimitExceeded", param: "couponCodes" }
1903
3654
  );
1904
3655
  }
1905
- const wireProducts = products ?? (0, import_shared7.foldIntoProducts)(items, subscriptions);
1906
- const sessionCurrency = (0, import_shared7.resolveSessionCurrency)(currency, items, subscriptions, wireProducts);
3656
+ const wireProducts = products ?? (0, import_shared8.foldIntoProducts)(items, subscriptions);
3657
+ const sessionCurrency = (0, import_shared8.resolveSessionCurrency)(currency, items, subscriptions, wireProducts);
1907
3658
  if (!sessionCurrency) {
1908
- throw new import_shared7.FloPayError(
3659
+ throw new import_shared8.FloPayError(
1909
3660
  "currency is required: pass `currency` on the session, or include a `currency` on the first item/subscription/product.",
1910
3661
  "validation_error",
1911
3662
  { code: "CurrencyRequired", param: "currency" }
@@ -1913,12 +3664,12 @@ async function createCheckoutSession(options) {
1913
3664
  }
1914
3665
  const payload = {
1915
3666
  clientId,
1916
- checkoutVersion: import_shared7.SDK_VERSION,
3667
+ checkoutVersion: import_shared8.SDK_VERSION,
1917
3668
  successUrl,
1918
3669
  cancelUrl,
1919
3670
  currency: sessionCurrency,
1920
3671
  checkoutMode,
1921
- products: wireProducts.map((product) => (0, import_shared7.buildProductPayload)(product, sessionCurrency)),
3672
+ products: wireProducts.map((product) => (0, import_shared8.buildProductPayload)(product, sessionCurrency)),
1922
3673
  accountData: {
1923
3674
  userId: account.userId,
1924
3675
  firstName: account.firstName ?? null,
@@ -1945,7 +3696,7 @@ async function createCheckoutSession(options) {
1945
3696
  const timer = setTimeout(() => controller.abort(), timeoutMs);
1946
3697
  const headers = { "Content-Type": "application/json" };
1947
3698
  if (resolvedIdempotencyKey) {
1948
- headers[import_shared7.IDEMPOTENCY_KEY_HEADER] = resolvedIdempotencyKey;
3699
+ headers[import_shared8.IDEMPOTENCY_KEY_HEADER] = resolvedIdempotencyKey;
1949
3700
  }
1950
3701
  let status;
1951
3702
  let body;
@@ -1956,6 +3707,10 @@ async function createCheckoutSession(options) {
1956
3707
  body: JSON.stringify(payload),
1957
3708
  signal: controller.signal
1958
3709
  });
3710
+ try {
3711
+ onFirstByte?.(response.status);
3712
+ } catch {
3713
+ }
1959
3714
  status = response.status;
1960
3715
  try {
1961
3716
  body = await response.json();
@@ -1974,7 +3729,7 @@ async function createCheckoutSession(options) {
1974
3729
  throw new Error("Checkout session created but no UUID was returned by the billing API");
1975
3730
  }
1976
3731
  if (!nonce) {
1977
- throw new import_shared7.FloPayError(
3732
+ throw new import_shared8.FloPayError(
1978
3733
  "Checkout session created but no `nonce` was returned by the billing API. Upgrade the billing service to TeamFloPay/backend#640 or later.",
1979
3734
  "api_error",
1980
3735
  { code: "MissingCheckoutSessionToken" }
@@ -2017,30 +3772,137 @@ async function createCheckoutSession(options) {
2017
3772
  }
2018
3773
  return { status };
2019
3774
  }
3775
+ async function createCheckoutSession(options) {
3776
+ const telemetry = beginCreateSessionTelemetry(options);
3777
+ const recordFirstByte = createFirstByteRecorder(telemetry);
3778
+ try {
3779
+ const result = await createCheckoutSessionCore(options, recordFirstByte);
3780
+ completeCreateSessionTelemetry(telemetry, result);
3781
+ return result;
3782
+ } catch (error) {
3783
+ failCreateSessionTelemetry(telemetry.reporter, error);
3784
+ throw error;
3785
+ } finally {
3786
+ finishCreateSessionTelemetry(telemetry.reporter);
3787
+ }
3788
+ }
3789
+ function createFirstByteRecorder(telemetry) {
3790
+ let recorded = false;
3791
+ return (status) => {
3792
+ if (recorded) return;
3793
+ recorded = true;
3794
+ const statusClass = `${Math.floor(status / 100)}xx`;
3795
+ telemetry.reporter.log({
3796
+ name: "session.request.first_byte",
3797
+ stage: "session_first_byte",
3798
+ requestCategory: "session_create",
3799
+ statusClass
3800
+ });
3801
+ telemetry.reporter.performance({
3802
+ stage: "session_first_byte",
3803
+ durationMs: telemetry.reporter.now() - telemetry.startedAt,
3804
+ durationMode: "machine",
3805
+ requestCategory: "session_create",
3806
+ statusClass
3807
+ });
3808
+ };
3809
+ }
3810
+ function beginCreateSessionTelemetry(options) {
3811
+ const reporter = new TelemetryReporter({
3812
+ billingApiUrl: options.billingApiUrl,
3813
+ sdkVersion: import_shared8.SDK_VERSION,
3814
+ enabled: options.telemetry !== false
3815
+ });
3816
+ const startedAt = reporter.now();
3817
+ reporter.log({
3818
+ name: "session.create.started",
3819
+ stage: "session_create",
3820
+ requestCategory: "session_create"
3821
+ });
3822
+ return { reporter, startedAt };
3823
+ }
3824
+ function completeCreateSessionTelemetry(telemetry, result) {
3825
+ const statusClass = `${Math.floor(result.status / 100)}xx`;
3826
+ telemetry.reporter.log({
3827
+ name: "session.request.completed",
3828
+ stage: "session_complete",
3829
+ requestCategory: "session_create",
3830
+ statusClass
3831
+ });
3832
+ telemetry.reporter.performance({
3833
+ stage: "session_complete",
3834
+ durationMs: telemetry.reporter.now() - telemetry.startedAt,
3835
+ durationMode: "machine",
3836
+ requestCategory: "session_create",
3837
+ statusClass
3838
+ });
3839
+ }
3840
+ function failCreateSessionTelemetry(reporter, error) {
3841
+ if (error instanceof import_shared8.FloPayError && error.type === "validation_error") {
3842
+ reporter.terminal({
3843
+ outcome: "validation_rejected",
3844
+ stage: "session_create",
3845
+ requestCategory: "session_create"
3846
+ });
3847
+ return;
3848
+ }
3849
+ const statusCode = error instanceof import_shared8.FloPayError ? error.statusCode : void 0;
3850
+ reporter.error({
3851
+ errorCode: error instanceof Error && error.name === "AbortError" ? "REQUEST_TIMEOUT" : error instanceof TypeError ? "NETWORK_REQUEST_FAILED" : "CHECKOUT_SESSION_CREATE_FAILED",
3852
+ stage: "session_create",
3853
+ requestCategory: "session_create",
3854
+ statusClass: error instanceof Error && error.name === "AbortError" ? "timeout" : statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
3855
+ });
3856
+ }
3857
+ function finishCreateSessionTelemetry(reporter) {
3858
+ void reporter.flush().catch(() => {
3859
+ }).finally(() => reporter.destroy());
3860
+ }
2020
3861
  async function createCheckoutSessionWithRetries(options) {
2021
3862
  const { maxRetries = 3, ...sessionOptions } = options;
3863
+ const telemetry = beginCreateSessionTelemetry(options);
3864
+ const recordFirstByte = createFirstByteRecorder(telemetry);
2022
3865
  if (!Number.isFinite(maxRetries) || !Number.isInteger(maxRetries) || maxRetries <= 0) {
3866
+ telemetry.reporter.terminal({
3867
+ outcome: "validation_rejected",
3868
+ stage: "session_create",
3869
+ requestCategory: "session_create"
3870
+ });
3871
+ finishCreateSessionTelemetry(telemetry.reporter);
2023
3872
  throw new Error("Number of retries must be greater than 0");
2024
3873
  }
2025
3874
  const attemptOptions = {
2026
3875
  ...sessionOptions,
2027
- idempotencyKey: (0, import_shared7.resolveIdempotencyKey)(sessionOptions.idempotencyKey)
3876
+ idempotencyKey: (0, import_shared8.resolveIdempotencyKey)(sessionOptions.idempotencyKey)
2028
3877
  };
2029
3878
  let lastErr;
2030
3879
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
2031
3880
  try {
2032
- return await createCheckoutSession(attemptOptions);
3881
+ const result = await createCheckoutSessionCore(attemptOptions, recordFirstByte);
3882
+ completeCreateSessionTelemetry(telemetry, result);
3883
+ finishCreateSessionTelemetry(telemetry.reporter);
3884
+ return result;
2033
3885
  } catch (err) {
2034
3886
  lastErr = err;
2035
3887
  const isTransportAbort = err instanceof Error && err.name === "AbortError";
2036
- const isInProgressReplay = err instanceof import_shared7.FloPayError && err.code === import_shared7.IDEMPOTENCY_IN_PROGRESS_CODE;
3888
+ const isInProgressReplay = err instanceof import_shared8.FloPayError && err.code === import_shared8.IDEMPOTENCY_IN_PROGRESS_CODE;
2037
3889
  if ((isTransportAbort || isInProgressReplay) && attempt < maxRetries) {
3890
+ telemetry.reporter.log({
3891
+ name: "operation.retry",
3892
+ stage: "session_create",
3893
+ requestCategory: "session_create",
3894
+ attempt: attempt + 1
3895
+ });
2038
3896
  await new Promise((r) => setTimeout(r, 100 * Math.pow(2, attempt)));
2039
3897
  continue;
2040
3898
  }
3899
+ failCreateSessionTelemetry(telemetry.reporter, err);
3900
+ finishCreateSessionTelemetry(telemetry.reporter);
2041
3901
  throw err;
2042
3902
  }
2043
3903
  }
3904
+ failCreateSessionTelemetry(telemetry.reporter, lastErr);
3905
+ finishCreateSessionTelemetry(telemetry.reporter);
2044
3906
  throw lastErr ?? new Error("Unknown error during checkout session creation");
2045
3907
  }
2046
3908
  // Annotate the CommonJS export names for ESM import in node: