Skip to content

feat: track captcha analytics#7822

Open
kernelwhisperer wants to merge 6 commits into
developfrom
feat/captcha-analytics
Open

feat: track captcha analytics#7822
kernelwhisperer wants to merge 6 commits into
developfrom
feat/captcha-analytics

Conversation

@kernelwhisperer

@kernelwhisperer kernelwhisperer commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds captcha analytics and simplifies shared analytics tracking.

  • Add createCowTracker() helper for category-scoped analytics events
  • Allow analytics event objects to carry custom params like reason
  • Move analytics transport failure handling into GTM data layer push
  • Replace GTM debug console.debug with logAnalytics
  • Track captcha challenge lifecycle events, excluding Turnstile demo site key traffic
  • Simplify affiliate analytics by relying on shared analytics handling

Summary by CodeRabbit

  • New Features

    • Added CAPTCHA event tracking to improve visibility into challenge progress, completion, and failure states.
    • Expanded analytics support for richer event details, including chain-aware events.
  • Bug Fixes

    • Improved analytics handling so event tracking is more resilient and won’t break user flows when logging fails.
    • Updated CAPTCHA-related analytics behavior to avoid tracking when the demo widget is used.

@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cowfi Ready Ready Preview Jul 9, 2026 2:53pm
explorer-dev Ready Ready Preview Jul 9, 2026 2:53pm
storybook Ready Ready Preview Jul 9, 2026 2:53pm
swap-dev Ready Ready Preview Jul 9, 2026 2:53pm
widget-configurator Ready Ready Preview Jul 9, 2026 2:53pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
cosmos Ignored Ignored Jul 9, 2026 2:53pm
sdk-tools Ignored Ignored Preview Jul 9, 2026 2:53pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR introduces AnalyticsEvent typing and a createCowTracker factory in the analytics library, wires CAPTCHA lifecycle event tracking into CaptchaWidget, simplifies affiliate analytics dispatch by removing error logging and field-filtering, hardens GTM data layer push with try/catch and logging, updates a Vercel COOP header to vary by environment, and adjusts related Jest mocks and tests.

Changes

Analytics tracking rework and CAPTCHA integration

Layer / File(s) Summary
Analytics type contracts and tracker factory
libs/analytics/src/CowAnalytics.ts, libs/analytics/src/createCowTracker.ts, libs/analytics/src/index.ts, libs/analytics/src/noop/createNoopCowAnalytics.ts, libs/common-utils/src/logger.ts
Adds AnalyticsEvent type and expanded EventOptions (with chainId), a createCowTracker factory re-exported from the library index, updates the noop implementation, and adds a logAnalytics logger.
GTM data layer push hardening
libs/analytics/src/gtm/CowAnalyticsGtm.ts, libs/analytics/src/gtm/CowAnalyticsGtm.test.ts
sendEvent accepts AnalyticsEvent, pushToDataLayer wraps push logic in try/catch with debug/warn logging via logAnalytics, and tests cover undefined-field omission and push-failure warnings.
CAPTCHA analytics tracking
apps/cowswap-frontend/src/common/analytics/types.ts, apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.tsx
Adds CAPTCHA analytics category and a trackCaptcha tracker enabled based on siteKey, instrumenting Turnstile callbacks to emit challenge started/solved/failed and interaction events.
Affiliate analytics simplification
apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.utils.ts, apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.test.ts, apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.test.tsx
Removes logAffiliate error logging and compactRecord filtering from trackAffiliateEvent, sends payload directly, updates test expectations, and drops a start-event-throws test.
Jest analytics mock consolidation
apps/cowswap-frontend/jest.setup.ts
Introduces a shared mockCowAnalytics.sendEvent spy used by both createCowTracker and useCowAnalytics mocks.

Vercel Cross-Origin-Opener-Policy header

Layer / File(s) Summary
COOP header environment switch
apps/cowswap-frontend/vercel.ts
Adds a crossOriginOpenerPolicy constant that resolves per VERCEL_ENV, applied to the Cross-Origin-Opener-Policy header instead of a hardcoded value.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Turnstile
  participant CaptchaWidget
  participant trackCaptcha
  participant CowAnalytics

  Turnstile->>CaptchaWidget: onWidgetLoad
  CaptchaWidget->>trackCaptcha: captcha_challenge_started
  trackCaptcha->>CowAnalytics: sendEvent

  Turnstile->>CaptchaWidget: onSuccess(jwt)
  CaptchaWidget->>trackCaptcha: captcha_challenge_solved
  trackCaptcha->>CowAnalytics: sendEvent

  Turnstile->>CaptchaWidget: onError / onExpire / onUnsupported
  CaptchaWidget->>trackCaptcha: captcha_challenge_failed(reason)
  trackCaptcha->>CowAnalytics: sendEvent
Loading

Suggested reviewers: alfetopito, shoom3301, Danziger

Poem

A rabbit taps the Turnstile gate, 🐰
Tracks each hop with data straight,
No more logs lost in the weeds,
Just clean events for CAPTCHA deeds,
Hop, sendEvent, hop again! 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description has a solid summary, but it misses the template's To Test and Background sections and any issue reference. Add the missing To Test checklist and Background section, and include a Fixes #issueNumber line if applicable.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately reflects the main change: captcha analytics tracking.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/captcha-analytics

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploying explorer-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: 60b2a0f
Status: ✅  Deploy successful!
Preview URL: https://df84b8dd.explorer-dev-dxz.pages.dev
Branch Preview URL: https://feat-captcha-analytics.explorer-dev-dxz.pages.dev

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploying swap-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: 60b2a0f
Status: ✅  Deploy successful!
Preview URL: https://d32d34db.swap-dev-5u6.pages.dev
Branch Preview URL: https://feat-captcha-analytics.swap-dev-5u6.pages.dev

View logs

## Summary

- Sets `Cross-Origin-Opener-Policy` to `unsafe-none` for non-production
Vercel deployments.
- Keeps production on `same-origin-allow-popups`.

## Why

Google Tag Assistant preview can lose its cross-origin opener/debug
connection when the preview page sends a stricter COOP header. This
branch gives us an isolated Vercel preview URL to verify whether
relaxing COOP fixes the Tag Assistant connection without changing the
original captcha analytics PR branch.

## Preview

-
https://swap-dev-git-agent-gtm-tag-assistant-coop-preview-cowswap-dev.vercel.app/?gtm_debug=x
- Verified response header: `cross-origin-opener-policy: unsafe-none`

## Checks

- `pnpm exec prettier --check apps/cowswap-frontend/vercel.ts`
- `pnpm exec eslint apps/cowswap-frontend/vercel.ts`
- `pnpm exec tsc --noEmit --pretty false --skipLibCheck
--moduleResolution node16 --module Node16 --target ES2022
apps/cowswap-frontend/vercel.ts`
- `curl -sI
'https://swap-dev-git-agent-gtm-tag-assistant-coop-preview-cowswap-dev.vercel.app/?gtm_debug=x'`
@kernelwhisperer
kernelwhisperer requested a review from a team July 9, 2026 14:14
@kernelwhisperer
kernelwhisperer marked this pull request as ready for review July 9, 2026 14:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.tsx (2)

141-147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Include errorCode in the analytics event.

onError receives errorCode and logs it, but doesn't pass it to trackCaptcha. Including it as a custom param would make captcha_challenge_failed events with reason: 'turnstileError' actionable in analytics dashboards without cross-referencing logs.

♻️ Pass errorCode to trackCaptcha
       onError={(errorCode) => {
         exchangeRequestIdRef.current += 1

         logCaptcha.error('Challenge errored', { errorCode, hostname: window.location.hostname })
-        trackCaptcha({ action: 'captcha_challenge_failed', reason: 'turnstileError' })
+        trackCaptcha({ action: 'captcha_challenge_failed', reason: 'turnstileError', errorCode })
         setCaptchaJwt(null)
       }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.tsx`
around lines 141 - 147, The CaptchaWidget.container.tsx onError handler logs
errorCode but does not send it with the captcha analytics event, so the failure
data is incomplete. Update the onError callback in CaptchaWidget.container to
pass errorCode through to trackCaptcha as an additional custom param alongside
action and reason, keeping the existing logCaptcha.error call unchanged.

29-31: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider memoizing trackCaptcha.

createCowTracker is called in the component body, producing a new closure on every render. Since Turnstile uses key={siteKey}, callbacks are refreshed on siteKey change regardless, so this is not a bug — but wrapping in useMemo keyed on siteKey avoids needless allocations and makes the enabled-dependency explicit.

♻️ Wrap in useMemo
+  const trackCaptcha = useMemo(
+    () => createCowTracker(CowSwapAnalyticsCategory.CAPTCHA, {
+      enabled: siteKey !== TURNSTILE_DEMO_INTERACTIVE_SITE_KEY,
+    }),
+    [siteKey]
+  )
-  const trackCaptcha = createCowTracker(CowSwapAnalyticsCategory.CAPTCHA, {
-    enabled: siteKey !== TURNSTILE_DEMO_INTERACTIVE_SITE_KEY,
-  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.tsx`
around lines 29 - 31, The `trackCaptcha` callback in `CaptchaWidget.container`
is recreated on every render because `createCowTracker` is called directly in
the component body. Wrap the `createCowTracker(CowSwapAnalyticsCategory.CAPTCHA,
{ enabled: siteKey !== TURNSTILE_DEMO_INTERACTIVE_SITE_KEY })` call in
`useMemo`, keyed on `siteKey`, so the closure is only rebuilt when the site key
changes and the `enabled` dependency is explicit.
libs/analytics/src/CowAnalytics.ts (1)

3-3: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant union member in AnalyticsEvent.

EventOptions & Record<string, unknown> is assignable to EventOptions, so the third union member adds no new type-level coverage. The union collapses to string | EventOptions. If the intent is to signal that arbitrary extra keys are allowed, a comment or a dedicated branded type would be clearer than a redundant intersection.

♻️ Simplify the union
-export type AnalyticsEvent = string | EventOptions | (EventOptions & Record<string, unknown>)
+export type AnalyticsEvent = string | EventOptions
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/analytics/src/CowAnalytics.ts` at line 3, Simplify the AnalyticsEvent
type by removing the redundant EventOptions & Record<string, unknown> union
member, since it is already covered by EventOptions. Update the exported type in
CowAnalytics to use only string | EventOptions, and if extra arbitrary fields
are intended, make that intent explicit elsewhere with a clearer type or comment
rather than keeping an equivalent union branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.utils.ts`:
- Around line 31-38: The `trackAffiliateEvent` helper is masking a `chainId`
type mismatch with an `as GtmEvent` cast, allowing invalid numeric values to
reach `analytics.sendEvent`. Fix this in `trackAffiliateEvent` by removing the
cast-based bypass and either narrowing `TrackAffiliateEventParams.chainId` to
`SupportedChainId` or validating/coercing `chainId` before building the event
payload so the `GtmEvent` type is satisfied without unsafe assertions.

---

Nitpick comments:
In
`@apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.tsx`:
- Around line 141-147: The CaptchaWidget.container.tsx onError handler logs
errorCode but does not send it with the captcha analytics event, so the failure
data is incomplete. Update the onError callback in CaptchaWidget.container to
pass errorCode through to trackCaptcha as an additional custom param alongside
action and reason, keeping the existing logCaptcha.error call unchanged.
- Around line 29-31: The `trackCaptcha` callback in `CaptchaWidget.container` is
recreated on every render because `createCowTracker` is called directly in the
component body. Wrap the `createCowTracker(CowSwapAnalyticsCategory.CAPTCHA, {
enabled: siteKey !== TURNSTILE_DEMO_INTERACTIVE_SITE_KEY })` call in `useMemo`,
keyed on `siteKey`, so the closure is only rebuilt when the site key changes and
the `enabled` dependency is explicit.

In `@libs/analytics/src/CowAnalytics.ts`:
- Line 3: Simplify the AnalyticsEvent type by removing the redundant
EventOptions & Record<string, unknown> union member, since it is already covered
by EventOptions. Update the exported type in CowAnalytics to use only string |
EventOptions, and if extra arbitrary fields are intended, make that intent
explicit elsewhere with a clearer type or comment rather than keeping an
equivalent union branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a4f68aa5-6f3e-42ff-9343-5cbe96d382e8

📥 Commits

Reviewing files that changed from the base of the PR and between 4b8c6af and e675478.

📒 Files selected for processing (14)
  • apps/cowswap-frontend/jest.setup.ts
  • apps/cowswap-frontend/src/common/analytics/types.ts
  • apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.test.ts
  • apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.utils.ts
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.test.tsx
  • apps/cowswap-frontend/src/modules/captcha/containers/CaptchaWidget.container.tsx
  • apps/cowswap-frontend/vercel.ts
  • libs/analytics/src/CowAnalytics.ts
  • libs/analytics/src/createCowTracker.ts
  • libs/analytics/src/gtm/CowAnalyticsGtm.test.ts
  • libs/analytics/src/gtm/CowAnalyticsGtm.ts
  • libs/analytics/src/index.ts
  • libs/analytics/src/noop/createNoopCowAnalytics.ts
  • libs/common-utils/src/logger.ts
💤 Files with no reviewable changes (1)
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.test.tsx

Comment on lines 31 to 38
export function trackAffiliateEvent({ analytics, action, chainId, ...customParams }: TrackAffiliateEventParams): void {
try {
analytics.sendEvent(
compactRecord({
category: CowSwapAnalyticsCategory.AFFILIATE,
action,
chainId,
...customParams,
}) as GtmEvent<CowSwapAnalyticsCategory.AFFILIATE>,
)
} catch (error) {
logAffiliate('Failed to send analytics event', { action, error })
}
analytics.sendEvent({
category: CowSwapAnalyticsCategory.AFFILIATE,
action,
chainId,
...customParams,
} as GtmEvent<CowSwapAnalyticsCategory.AFFILIATE>)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

as GtmEvent cast bypasses chainId type mismatch.

TrackAffiliateEventParams.chainId is typed SupportedChainId | number, but EventOptions.chainId expects SupportedChainId. The cast silently widens invalid numeric values through to the analytics payload. Either narrow the parameter type or validate at the boundary.

🛡️ Narrow chainId type instead of casting
 interface TrackAffiliateEventParams {
   analytics: CowAnalytics
   action: AffiliateAnalyticsAction
-  chainId?: SupportedChainId | number
+  chainId?: SupportedChainId
   [key: string]: unknown
 }

If raw number support is intentional, validate before sending:

 export function trackAffiliateEvent({ analytics, action, chainId, ...customParams }: TrackAffiliateEventParams): void {
+  if (chainId !== undefined && !Object.values(SupportedChainId).includes(chainId as SupportedChainId)) {
+    return
+  }
   analytics.sendEvent({
     category: CowSwapAnalyticsCategory.AFFILIATE,
     action,
     chainId,
     ...customParams,
   } as GtmEvent<CowSwapAnalyticsCategory.AFFILIATE>)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.utils.ts`
around lines 31 - 38, The `trackAffiliateEvent` helper is masking a `chainId`
type mismatch with an `as GtmEvent` cast, allowing invalid numeric values to
reach `analytics.sendEvent`. Fix this in `trackAffiliateEvent` by removing the
cast-based bypass and either narrowing `TrackAffiliateEventParams.chainId` to
`SupportedChainId` or validating/coercing `chainId` before building the event
payload so the `GtmEvent` type is satisfied without unsafe assertions.

@fairlighteth fairlighteth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ AI Review (Codex GPT-5, worked 10m): question on reserved GTM custom-param names

Finding: [QUESTION] Should custom analytics params protect GTM’s reserved names?

  • Location: libs/analytics/src/gtm/CowAnalyticsGtm.ts:264
  • I do not see a current regression in this PR: the new captcha/affiliate events use custom fields like reason, result, and failureReason.
  • The edge case is the new generic custom-param path. GTM uses event as the event name, and this code builds official fields first, then spreads getAdditionalEventParams(...) last. A future caller passing { event: 'wrong_name' } could overwrite the canonical event: event.action.
  • This pattern is not new only in this PR: toGtmEvent() has a similar “official fields first, extra fields last” shape. The difference is that this PR makes arbitrary custom params a first-class path through AnalyticsEvent / createCowTracker, so this may be a good moment to define the reserved-key behavior centrally.

Suggested fix

  • Filter reserved GTM output keys like event from custom params, or spread custom params before the official GTM fields so official fields always win.
  • Add a small CowAnalyticsGtm unit test showing { action: 'captcha_challenge_failed', event: 'wrong_name' } still sends event: 'captcha_challenge_failed'.
Review scope and related context

This is separate from existing review comments:

  • CodeRabbit already covers the affiliate chainId cast.
  • CodeRabbit’s captcha errorCode note is about richer analytics metadata; this question is about protecting reserved GTM output fields.
  • This is not framed as a current PR regression, only as an API-contract question now that custom params are easier to send.
🤖 Prompt for AI agents
Verify this finding against current code. Fix only if still valid, keep the change minimal, and validate with a targeted unit test.

Context:
- libs/analytics/src/CowAnalytics.ts now allows custom params through AnalyticsEvent.
- libs/analytics/src/createCowTracker.ts forwards arbitrary custom params.
- libs/analytics/src/gtm/CowAnalyticsGtm.ts spreads getAdditionalEventParams(gtmEvent) after canonical fields, so a custom `event` key could override the GTM event name.
- toGtmEvent() has a similar shape, so prefer a central reserved-key policy if practical.

Generated using the pr-review skill from the CoW Protocol skills repo.

const [siteKey, setSiteKey] = useState(TURNSTILE_SITE_KEY)
const theme = useTheme()

const trackCaptcha = createCowTracker(CowSwapAnalyticsCategory.CAPTCHA, {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

createCowTracker returns a new function every time, so it should be memoized

// Isolates the browsing context so cross-origin pages cannot access window.opener,
// while still allowing this page to open popups (needed for wallet connections).
{ key: 'Cross-Origin-Opener-Policy', value: 'same-origin-allow-popups' },
// Preview deploys use unsafe-none so Tag Assistant can keep its cross-origin debug connection.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It might be a bit dangerous in case if something depends on Cross-Origin-Opener-Policy and we don't notice it locally because it's unsafe-none

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.

3 participants