Skip to content

chore(telemetry): add payment funnel spans for Apple Pay and Google Wallet#53

Merged
arstiefel merged 2 commits intomainfrom
chore-telemetry-add-payment
Apr 24, 2026
Merged

chore(telemetry): add payment funnel spans for Apple Pay and Google Wallet#53
arstiefel merged 2 commits intomainfrom
chore-telemetry-add-payment

Conversation

@arstiefel
Copy link
Copy Markdown
Collaborator

@arstiefel arstiefel commented Apr 16, 2026

Description

Closes the payment funnel observability gap identified in the analytics & metrics analysis. Adds granular OTel funnel markers (button press, tokenize success/failure, user cancellation) across Apple Pay (WebView + native) and Google Wallet, bringing RN SDK span coverage in line with the web SDK's Counter/EventTracker funnel. Funnel markers inside an operation are attached to the parent request_payment span via span.addEvent(...) so they correlate naturally in Grafana; standalone pre-operation events (button_pressed) go through a shared recordEvent(name, attrs) helper in telemetry/tracer.ts. Also adds bolt.platform='react-native' to the OTel resource so RN traffic can be filtered separately from the web SDK.

Along the way, fixes correctness issues in the native iOS ApplePayModule: the JS promise could previously leak on user cancel, on PassKit presentation failure, and on dismiss-completion drops. All mutation of pendingResolve / pendingReject / promiseSettled is now serialized on the main queue (including the initial reset, which previously ran on the RN bridge method queue and raced with PassKit delegate callbacks); present(completion:) is used and rejects PRESENT_FAILED on !presented; the synthesized CANCELLED settlement in paymentAuthorizationControllerDidFinish is emitted outside the (unreliable) dismiss { } completion block; and settleReject now logs when it fires with a nil pendingReject rather than silently flipping the guard. Cancel is now silent across all three payment surfaces (no onError, SpanStatusCode.UNSET on the parent span) — matching the WebView's pre-existing behavior and bringing Google Wallet (which previously conflated cancel with tokenize failure) into alignment.

Testing

All 162 Jest tests pass (155 pre-existing + 7 new). iOS Swift typechecks clean (swiftc -typecheck). Coverage added in this PR:

  • src/__tests__/ApplePay.test.tsx — silent CANCELLED branch (native rejection with code: 'CANCELLED' → no onError, no tokenize call, no reportAuthorizationResult call, bolt.apple_pay.cancelled event on parent span); button_pressed event emission; tokenize_success span-event emission on the happy path. The pre-existing "User cancelled" test (which used new Error('User cancelled') without a .code) is retained and renamed to cover the generic-native-rejection fall-through path.
  • src/__tests__/GoogleWallet.test.tsx — mirror CANCELLED coverage and funnel-marker assertions, plus the renamed generic-rejection test.
  • Span mock upgraded from a shared stub (startSpan: () => mockSpan) to jest.fn((name, attrs) => mockSpan) so tests can differentiate spans by name and assert on mockSpan.addEvent(name, attrs) instead of the prior lossy "any span fired" shape.

Native iOS Swift coverage remains a gap — no iOS test target exists in the repo. The new state machine's invariants (at-most-once settlement, main-queue serialization, presentation-failure rejection, dismiss-drop resilience) are only validated through manual iOS simulator runs against the PassKit sandbox.

Security Review

Important

A security review is required for every PR in this repository to comply with PCI requirements.

  • I have considered and reviewed security implications of this PR and included the summary below.

Security Impact Summary

The publishable-key prefix logged to OTel is widened from 8 characters + ... to 12 characters + ... in src/telemetry/setup.ts. The publishable key is a non-secret, merchant-facing identifier (equivalent to a Stripe publishable key) and is already sent in plaintext in every API request and WebView URL — logging a slightly longer prefix to Grafana Cloud does not increase exposure or change the risk posture. The value remains truncated; the full key is not logged. No changes to authentication, authorization, tokenization logic, payment-data handling, or PCI-scoped storage. The native Apple Pay changes are correctness fixes to the promise state machine — no change to which fields of PKPayment are serialized or how the tokenizer API is called.

@arstiefel arstiefel requested review from a team as code owners April 16, 2026 18:33
@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io Bot commented Apr 16, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds React Native SDK payment-funnel observability via new OpenTelemetry spans across Apple Pay (native + WebView) and Google Wallet, and fixes an iOS native Apple Pay cancellation edge case so JS promises don’t remain unresolved. Also augments telemetry resource attributes to better segment RN traffic and adjusts release tooling/changelog behavior.

Changes:

  • Add granular payment funnel spans (button press, tokenize success/failure, cancellation) for Apple Pay and Google Wallet.
  • Fix iOS native Apple Pay module to reject the JS promise on user dismissal/cancellation.
  • Add bolt.platform='react-native' (and log full publishable key) on the OTel resource; hide chore entries in release-it changelog sections.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/telemetry/setup.ts Adds bolt.platform resource attribute and switches publishable key attribute to full value.
src/telemetry/attributes.ts Introduces new telemetry attribute keys (bolt.platform, payment.cancelled).
src/payments/GoogleWallet.tsx Adds spans for button press/tokenize success/failure and a new cancellation branch.
src/payments/ApplePay.tsx Adds spans for button press/tokenize success/failure and cancellation handling for native flow.
src/payments/ApplePayWebView.tsx Emits a cancellation span for WebView Apple Pay cancellation events.
ios/ApplePayModule.swift Adds promise settlement tracking and rejects on sheet dismissal when not otherwise settled.
example/ios/Podfile.lock Updates example app pod lock to the new SDK version checksum.
.release-it.json Hides chore entries from generated changelog sections.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/payments/GoogleWallet.tsx
Comment thread ios/ApplePayModule.swift Outdated
Comment thread src/telemetry/setup.ts
@arstiefel arstiefel force-pushed the chore-telemetry-add-payment branch 4 times, most recently from 18ff9fe to 1891c71 Compare April 21, 2026 13:03
@arstiefel arstiefel force-pushed the chore-telemetry-add-payment branch from 1891c71 to f9a6f97 Compare April 23, 2026 20:20
iOS ApplePayModule:
- Move pendingResolve/pendingReject/promiseSettled writes onto the main
  queue so the single-thread invariant the docstring promises actually
  holds. Previously the initial reset ran on the RN bridge queue,
  creating a race with main-queue PassKit delegate callbacks.
- Use present(completion:) and reject with PRESENT_FAILED when PassKit
  can't present the sheet (invalid merchantId, missing entitlement,
  etc.) — otherwise neither didAuthorize nor didFinish ever fires and
  the JS promise hangs forever.
- Move the synthesized CANCELLED settleReject outside the dismiss
  completion block. Apple's dismiss completion is not reliably
  invoked in every path, so keeping the settlement inside it is a
  leak risk.
- Log when settleReject runs with no pendingReject instead of
  silently flipping the promiseSettled guard.
- Trim redundant comments.

JS:
- Cancellation is now silent across all three payment surfaces
  (Apple Pay native, Apple Pay WebView, Google Pay) — no onError
  call, to match the WebView's pre-existing behavior and avoid
  surfacing "User cancelled" to merchant error handlers.
- Collapse zero-duration tokenize_success / tokenize_failure /
  cancelled child spans into parentSpan.addEvent(...) on the
  request_payment span so funnel markers correlate with the parent.
- Add a shared recordEvent() helper in tracer.ts for standalone
  funnel markers (button_pressed) that precede the parent span.

Tests:
- Upgrade the startSpan mock to a jest.fn that captures span names
  so tests can differentiate the spans being emitted.
- Add coverage for the native CANCELLED-code path on both Apple Pay
  and Google Wallet — the prior test used Error('User cancelled')
  without a .code property, which hit the fall-through tokenize_
  failure path rather than the new CANCELLED branch.
- Add coverage asserting button_pressed event and tokenize_success
  span-event emission.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arstiefel arstiefel merged commit eb2f3ba into main Apr 24, 2026
10 checks passed
@arstiefel arstiefel deleted the chore-telemetry-add-payment branch April 24, 2026 13:40
Copy link
Copy Markdown
Contributor

@SanthoshCharanBolt SanthoshCharanBolt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are we setting the session attributes? 👀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants