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