Skip to content

feat: add affiliate analytics#7303

Open
fairlighteth wants to merge 10 commits intodevelopfrom
feat/affiliate-analytics
Open

feat: add affiliate analytics#7303
fairlighteth wants to merge 10 commits intodevelopfrom
feat/affiliate-analytics

Conversation

@fairlighteth
Copy link
Copy Markdown
Contributor

@fairlighteth fairlighteth commented Apr 10, 2026

Summary

This PR adds affiliate frontend analytics to cowswap-frontend for the core partner and trader funnel.

It instruments:

  • affiliate partner page-state views
  • partner code creation started / completed
  • affiliate trader page-state views
  • trader modal opened / modal-state views
  • trader code verification started / completed
  • expired trader code viewed

The new events are:

  • affiliate_partner_page_state_viewed
  • affiliate_partner_code_create_started
  • affiliate_partner_code_create_completed
  • affiliate_trader_page_state_viewed
  • affiliate_trader_modal_opened
  • affiliate_trader_modal_state_viewed
  • affiliate_trader_code_verification_started
  • affiliate_trader_code_verification_completed
  • affiliate_trader_expired_code_viewed

This PR also includes the supporting logic needed to keep these analytics accurate and non-blocking:

  • analytics failures do not break partner / trader flows
  • partner and trader page-state analytics match the UI states being rendered
  • trader modal-open analytics fire once per actual open and wait for pending / recovery state to settle before sending

No product UI changes are intended.

To Test

  1. Partner affiliate funnel
  • Open the Affiliate Partner page while disconnected and verify affiliate_partner_page_state_viewed with the expected pageState
  • Open the Affiliate Partner page with no code and verify the code-creation state
  • Open the Affiliate Partner page with an existing code and verify the live state
  • Create a partner code and verify affiliate_partner_code_create_started and affiliate_partner_code_create_completed
  • Verify failed partner-code creation emits affiliate_partner_code_create_completed with the expected failure result / reason
  1. Trader affiliate funnel
  • Open the Affiliate Trader page with no saved code and verify affiliate_trader_page_state_viewed with the expected pageState
  • Verify trader page-state analytics on loading, unsupported, ineligible, and linked
  • Open the trader modal and verify affiliate_trader_modal_opened fires once per actual open
  • Verify affiliate_trader_modal_state_viewed reflects the modal state being shown
  • Verify a referral code and confirm affiliate_trader_code_verification_started and affiliate_trader_code_verification_completed
  • Try invalid / self-referral / unavailable verification paths and verify the completion result payloads
  • Open the trader page for an expired linked code and verify affiliate_trader_expired_code_viewed
  1. Analytics payload sanity
  • In GTM Preview or window.dataLayer, verify the event names above are emitted
  • Verify raw referral codes are not included in affiliate analytics payloads

Background

This PR focuses on frontend affiliate funnel analytics only.

A follow-up PR is planned for affiliate attribution on actual order submission flows:

  • swap
  • ETH flow
  • safe bundle swap
  • limit orders
  • safe bundle limit orders
  • TWAP

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 10, 2026

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

Project Deployment Actions Updated (UTC)
cowfi Ready Ready Preview Apr 17, 2026 2:49pm
explorer-dev Ready Ready Preview Apr 17, 2026 2:49pm
swap-dev Ready Ready Preview Apr 17, 2026 2:49pm
widget-configurator Ready Ready Preview Apr 17, 2026 2:49pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
cosmos Ignored Ignored Apr 17, 2026 2:49pm
sdk-tools Ignored Ignored Preview Apr 17, 2026 2:49pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds affiliate analytics types and centralized tracking, instruments affiliate UI and hooks with de-duplicated view/event reporting, refactors modal/saved-code atoms and storage migration, enhances verification/create hooks to include codeSource and richer params, and adds many tests across atoms, hooks, and components.

Changes

Cohort / File(s) Summary
Analytics Core
apps/cowswap-frontend/src/common/analytics/types.ts, apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.types.ts, apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.utils.ts, apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.test.ts
Added CowSwapAnalyticsCategory.AFFILIATE; new affiliate enums/types and union action type; trackAffiliateEvent (compacts undefineds, guarded send); view-key and state-derivation helpers; tests for payloads and hook behavior.
Modal & Saved-Code State
apps/cowswap-frontend/src/modules/affiliate/state/affiliateTraderModalAtom.ts, apps/cowswap-frontend/src/modules/affiliate/state/affiliateTraderModalAtom.test.ts, apps/cowswap-frontend/src/modules/affiliate/state/affiliateTraderSavedCodeAtom.ts, apps/cowswap-frontend/src/modules/affiliate/state/affiliateTraderSavedCodeAtom.test.ts, apps/cowswap-frontend/src/modules/affiliate/state/migrations/affiliateTraderSavedCodeStorage.utils.ts
Replaced boolean modal atom with structured { isOpen, source } and added open/close atoms; extended saved-code shape with source; introduced storage wrapper with legacy migration, normalization, address-key canonicalization; added tests.
Hooks & Verification
apps/cowswap-frontend/src/modules/affiliate/hooks/...
New useAffiliateStateViewAnalytics hook with de-duplication; verification/create/input hooks accept structured params (include codeSource, walletStatus, requiresPayoutConfirmation); routed verification/create flows through typed params; availability hook adds NetworkError state and emits analytics.
Partner Containers
apps/cowswap-frontend/src/modules/affiliate/containers/AffiliatePartnerOnboard.tsx, .../AffiliatePartnerCodeCreation.tsx, .../AffiliatePartnerCodeInfo.tsx, .../AffiliatePartnerQrModal.tsx
Instrumented onboarding CTAs, suggestion regeneration, copy/share/QR interactions with trackAffiliateEvent; extracted PartnerOnboardActions; wired onCopy/onOpen handlers to send analytics.
Trader Containers & Modals
apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderOnboard.tsx, .../AffiliateTraderCodeInfo.tsx, .../AffiliateTraderModal.tsx, .../AffiliateTraderModalCodeInfo.tsx, .../AffiliateTraderModalCodeLinking.tsx, .../AffiliateTraderModalIneligible.tsx, .../AffiliateTraderRewardsRow.tsx
Switched to open/close modal atoms and threaded AffiliateEntrySource; replaced inline analytics with trackAffiliateEvent; moved effect-based sends to view-hook usage; added richer event params (walletStatus, entrySource, codeSource, flags).
Config, Exports & Module Surface
apps/cowswap-frontend/src/modules/affiliate/config/affiliateProgram.const.ts, apps/cowswap-frontend/src/modules/affiliate/index.ts
Bumped saved-codes key v0→v1 and added legacy key constant; expanded module exports to include analytics types, utils, and useAffiliateStateViewAnalytics; re-exported isSupportedTradingNetwork.
Pages
apps/cowswap-frontend/src/pages/Account/AffiliatePartner.tsx, apps/cowswap-frontend/src/pages/Account/AffiliateTrader.tsx
Centralized page-state derivation via helpers and added view analytics calls using computed view keys and sanitized event params.
Misc Components & Tests
apps/cowswap-frontend/src/legacy/components/Copy/CopyMod.tsx, apps/cowswap-frontend/src/modules/affiliate/pure/HowItWorks.tsx, apps/cowswap-frontend/src/modules/affiliate/pure/AffiliatePartner/AffiliatePartnerCodeForm.tsx, plus many new/updated tests under apps/cowswap-frontend/src/modules/affiliate/...
CopyHelper: type-only React imports, added onCopy?: () => void, explicit return type; HowItWorks accepts optional onClick; mapped NetworkError to error adornment; large test additions for analytics, atoms, hooks, and create-flow concurrency.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UI as Component
  participant Analytics as AnalyticsClient
  participant Storage as LocalStorage/Atoms

  rect rgba(200,200,255,0.5)
  User->>UI: click/copy/open modal/submit
  end

  rect rgba(200,255,200,0.5)
  UI->>Analytics: trackAffiliateEvent(action, params)
  Analytics->>Analytics: compact payload (remove undefined)
  Analytics->>Analytics: sendEvent(category: 'Affiliate', action, params)
  end

  rect rgba(255,230,200,0.5)
  UI->>Storage: update atoms / persist saved code (with source)
  Storage-->>UI: state updated / ack
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped through atoms, keys, and views,
I tracked each click and copied news,
Events compacted, sources found,
Modals open, metrics bound,
A little rabbit logs with joy.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add affiliate analytics' is concise and directly describes the main change: adding analytics instrumentation for the affiliate feature.
Description check ✅ Passed The pull request description is comprehensive and well-structured, following the template with clear sections for Summary, To Test, and Background.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/affiliate-analytics

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.ts (1)

42-90: ⚠️ Potential issue | 🟠 Major

Guard onCreate against double submission.

This callback can be entered twice before the button disables, which means duplicate signature prompts and concurrent createCode(...) writes. Because this is a create flow, it should have an in-flight guard inside the hook, not only in the UI.

Suggested fix
-import { useCallback, useMemo, useState } from 'react'
+import { useCallback, useMemo, useRef, useState } from 'react'
...
 export function useAffiliatePartnerCodeCreate({
   account,
   provider,
   code,
   setError,
 }: UseAffiliatePartnerCodeCreateParams): UseAffiliatePartnerCodeCreateResult {
   const analytics = useCowAnalytics()
+  const submittingRef = useRef(false)
   const [submitting, setSubmitting] = useState(false)
   const { mutate: mutatePartnerInfo } = useAffiliatePartnerInfo(account)

   const onCreate = useCallback(async (): Promise<void> => {
-    if (!account || !provider) return
+    if (!account || !provider || submittingRef.current) return
+
+    submittingRef.current = true

     trackAffiliateEvent({
       analytics,
       action: 'affiliate_partner_code_create_started',
       chainId: AFFILIATE_PAYOUTS_CHAIN_ID,
@@
     } finally {
+      submittingRef.current = false
       setSubmitting(false)
     }
-  }, [account, analytics, code, mutatePartnerInfo, provider, setError])
+  }, [account, analytics, code, mutatePartnerInfo, provider, setError])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.ts`
around lines 42 - 90, The onCreate callback can run concurrently and must guard
against double submission: add an in-flight guard at the top of onCreate that
returns early if a submission is already in progress (use the existing
submitting state or, better, a mutable ref like isSubmittingRef to avoid stale
closure issues), set the guard true immediately before starting the try block,
and ensure it is reset to false in the finally block; keep the rest of the flow
(provider.getSigner(), signer._signTypedData, bffAffiliateApi.createCode,
trackAffiliateEvent, mutatePartnerInfo, setError) unchanged and reference
onCreate, setSubmitting, submitting (or isSubmittingRef) and mutatePartnerInfo
when making the change.
🧹 Nitpick comments (3)
apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderCodeInfo.tsx (1)

123-123: Extract the inline click handler for modal open.

Line 123 introduces a render-time inline factory. Prefer a stable callback for consistency with render-stability rules.

♻️ Suggested refactor
-import { ReactNode, useMemo } from 'react'
+import { ReactNode, useCallback, useMemo } from 'react'
@@
   const openAffiliateModal = useSetAtom(openTraderModalAtom)
+  const onEditCodeClick = useCallback((): void => {
+    openAffiliateModal(AffiliateEntrySource.TRADER_PAGE_CODE_CARD)
+  }, [openAffiliateModal])
@@
-              <ButtonPrimary onClick={() => openAffiliateModal(AffiliateEntrySource.TRADER_PAGE_CODE_CARD)}>
+              <ButtonPrimary onClick={onEditCodeClick}>

As per coding guidelines: Replace inline factories with extracted components or memoized callbacks; obey react/no-unstable-nested-components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderCodeInfo.tsx`
at line 123, The inline click handler passed to ButtonPrimary in
AffiliateTraderCodeInfo should be extracted into a stable callback to avoid
render-time inline factories; create a memoized handler (e.g., const
handleOpenAffiliateModal = useCallback(() =>
openAffiliateModal(AffiliateEntrySource.TRADER_PAGE_CODE_CARD),
[openAffiliateModal])) inside the AffiliateTraderCodeInfo component and pass it
to ButtonPrimary via onClick={handleOpenAffiliateModal}; ensure React's
useCallback is imported and that AffiliateEntrySource.TRADER_PAGE_CODE_CARD and
openAffiliateModal identifiers are used exactly as in the current file.
apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeAvailability.ts (1)

48-59: Consider migrating from SWR to atomWithQuery.

Per coding guidelines, SWR is deprecated for new data fetching. This existing SWR flow could be migrated to Jotai's atomWithQuery in a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeAvailability.ts`
around lines 48 - 59, Replace the SWR hook in
useAffiliatePartnerCodeAvailability.ts with a Jotai atom created via
atomWithQuery: remove useSWR call that defines data:isAvailable, error,
isLoading and instead create an atom that takes debouncedCode and
waitingForDebouncedInput as inputs and uses
bffAffiliateApi.verifyCodeAvailability(code) as the fetcher; then expose derived
values (isAvailable, error, isLoading) from that atom and drop
SWR_NO_REFRESH_OPTIONS and shouldRetryOnError. Ensure the new atom key/inputs
mirror the previous conditional (null when waitingForDebouncedInput or no
debouncedCode) so no request is made, and reference the existing symbols
debouncedCode, waitingForDebouncedInput, and
bffAffiliateApi.verifyCodeAvailability to locate where to replace the useSWR
logic.
apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliateTraderCodeInput.ts (1)

103-109: Dependency array for onChange may be incomplete.

setCodeSource and shouldAutoVerify are used in resetTraderCodeInputState but not listed in deps. Since setCodeSource is a useState setter (stable) and shouldAutoVerify is a ref (stable identity), this works correctly, but including them would be more explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliateTraderCodeInput.ts`
around lines 103 - 109, The onChange callback’s dependency array is incomplete:
it calls resetTraderCodeInputState and uses setCodeSource and shouldAutoVerify
but only lists setError; update the useCallback dependencies for onChange to
include resetTraderCodeInputState, setCodeSource, and shouldAutoVerify (in
addition to setError) so the hook is explicit about all referenced symbols (even
though setCodeSource and shouldAutoVerify are stable).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.utils.ts`:
- Around line 62-81: The function getAffiliateTraderPageState currently sets
LINKED only when hasSavedCode is true, causing a walletStatus of
TraderWalletStatus.LINKED to be misclassified as ONBOARD if hasSavedCode is
false; update getAffiliateTraderPageState to explicitly check for
TraderWalletStatus.LINKED and return AffiliatePageState.LINKED (independent of
hasSavedCode) before the final default/other switches so the page-state
correctly reflects the wallet's LINKED status.

In
`@apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderModalCodeLinking.tsx`:
- Around line 66-90: onSubmit currently tracks clicks for the savedCode and
connect-to-verify branches but does not emit an analytics event before calling
verifyCode when the user is connected and has no saved code; add a
trackAffiliateEvent call (using the same analytics, entrySource, walletStatus
props) right before the void verifyCode(...) call with action:
'affiliate_trader_modal_primary_cta_clicked' and ctaType: 'verifyCode' so the
verify path is instrumented; update the onSubmit flow to call
trackAffiliateEvent(...), then call verifyCode({ code: codeInput, account,
codeSource }).

In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliateStateViewAnalytics.ts`:
- Around line 22-39: The hook useAffiliateStateViewAnalytics currently
deduplicates emissions using only lastViewKeyRef/current viewKey, causing
updates to eventParams to be ignored; change the dedupe to include the full
analytics signature (e.g., combine viewKey and eventParams or accept a
dedupeKey) so that when eventParams changes the effect re-triggers: update
lastViewKeyRef to store a composite key (or add lastPayloadRef) and modify the
equality check and dependencies (the effect's dependency array and compare logic
surrounding lastViewKeyRef, viewKey, eventParams, analytics) so
trackAffiliateEvent is called whenever any payload-affecting field changes;
ensure trackAffiliateEvent call sites remain unchanged.

In
`@apps/cowswap-frontend/src/modules/affiliate/state/migrations/affiliateTraderSavedCodeStorage.utils.ts`:
- Around line 96-98: The isRecord type guard currently treats arrays as objects;
update isRecord to exclude arrays by adding an explicit check (e.g., use
!Array.isArray(value)) so it only returns true for plain object records; locate
the isRecord function and change its boolean expression to include the array
exclusion to prevent arrays from being treated as Record<string, unknown>.

In `@apps/cowswap-frontend/src/pages/Account/AffiliateTrader.tsx`:
- Line 33: The constant hasSavedCode is using Boolean(savedCode); update the
assignment to use explicit double-negation (two exclamation marks) on savedCode
instead to match project convention — i.e., replace the Boolean conversion with
the !! savedCode style where hasSavedCode is defined.

---

Outside diff comments:
In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.ts`:
- Around line 42-90: The onCreate callback can run concurrently and must guard
against double submission: add an in-flight guard at the top of onCreate that
returns early if a submission is already in progress (use the existing
submitting state or, better, a mutable ref like isSubmittingRef to avoid stale
closure issues), set the guard true immediately before starting the try block,
and ensure it is reset to false in the finally block; keep the rest of the flow
(provider.getSigner(), signer._signTypedData, bffAffiliateApi.createCode,
trackAffiliateEvent, mutatePartnerInfo, setError) unchanged and reference
onCreate, setSubmitting, submitting (or isSubmittingRef) and mutatePartnerInfo
when making the change.

---

Nitpick comments:
In
`@apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderCodeInfo.tsx`:
- Line 123: The inline click handler passed to ButtonPrimary in
AffiliateTraderCodeInfo should be extracted into a stable callback to avoid
render-time inline factories; create a memoized handler (e.g., const
handleOpenAffiliateModal = useCallback(() =>
openAffiliateModal(AffiliateEntrySource.TRADER_PAGE_CODE_CARD),
[openAffiliateModal])) inside the AffiliateTraderCodeInfo component and pass it
to ButtonPrimary via onClick={handleOpenAffiliateModal}; ensure React's
useCallback is imported and that AffiliateEntrySource.TRADER_PAGE_CODE_CARD and
openAffiliateModal identifiers are used exactly as in the current file.

In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeAvailability.ts`:
- Around line 48-59: Replace the SWR hook in
useAffiliatePartnerCodeAvailability.ts with a Jotai atom created via
atomWithQuery: remove useSWR call that defines data:isAvailable, error,
isLoading and instead create an atom that takes debouncedCode and
waitingForDebouncedInput as inputs and uses
bffAffiliateApi.verifyCodeAvailability(code) as the fetcher; then expose derived
values (isAvailable, error, isLoading) from that atom and drop
SWR_NO_REFRESH_OPTIONS and shouldRetryOnError. Ensure the new atom key/inputs
mirror the previous conditional (null when waitingForDebouncedInput or no
debouncedCode) so no request is made, and reference the existing symbols
debouncedCode, waitingForDebouncedInput, and
bffAffiliateApi.verifyCodeAvailability to locate where to replace the useSWR
logic.

In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliateTraderCodeInput.ts`:
- Around line 103-109: The onChange callback’s dependency array is incomplete:
it calls resetTraderCodeInputState and uses setCodeSource and shouldAutoVerify
but only lists setError; update the useCallback dependencies for onChange to
include resetTraderCodeInputState, setCodeSource, and shouldAutoVerify (in
addition to setError) so the hook is explicit about all referenced symbols (even
though setCodeSource and shouldAutoVerify are stable).
🪄 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: 95c0ae3f-d317-4af4-b8c4-b3fd63f99928

📥 Commits

Reviewing files that changed from the base of the PR and between 2951d99 and 6d41584.

📒 Files selected for processing (34)
  • apps/cowswap-frontend/src/common/analytics/types.ts
  • apps/cowswap-frontend/src/legacy/components/Copy/CopyMod.tsx
  • apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.test.ts
  • apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.types.ts
  • apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.utils.ts
  • apps/cowswap-frontend/src/modules/affiliate/config/affiliateProgram.const.ts
  • apps/cowswap-frontend/src/modules/affiliate/containers/AffiliatePartnerCodeCreation.tsx
  • apps/cowswap-frontend/src/modules/affiliate/containers/AffiliatePartnerCodeInfo.tsx
  • apps/cowswap-frontend/src/modules/affiliate/containers/AffiliatePartnerOnboard.tsx
  • apps/cowswap-frontend/src/modules/affiliate/containers/AffiliatePartnerQrModal.tsx
  • apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderCodeInfo.tsx
  • apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderModal.tsx
  • apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderModalCodeInfo.tsx
  • apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderModalCodeLinking.tsx
  • apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderModalIneligible.tsx
  • apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderOnboard.tsx
  • apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderRewardsRow.tsx
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeAvailability.ts
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.ts
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliateStateViewAnalytics.ts
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliateTraderCodeInput.ts
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliateTraderRecoverySideEffect.ts
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliateTraderRefUrlSideEffect.ts
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliateTraderVerification.ts
  • apps/cowswap-frontend/src/modules/affiliate/index.ts
  • apps/cowswap-frontend/src/modules/affiliate/pure/AffiliatePartner/AffiliatePartnerCodeForm.tsx
  • apps/cowswap-frontend/src/modules/affiliate/pure/HowItWorks.tsx
  • apps/cowswap-frontend/src/modules/affiliate/state/affiliateTraderModalAtom.test.ts
  • apps/cowswap-frontend/src/modules/affiliate/state/affiliateTraderModalAtom.ts
  • apps/cowswap-frontend/src/modules/affiliate/state/affiliateTraderSavedCodeAtom.test.ts
  • apps/cowswap-frontend/src/modules/affiliate/state/affiliateTraderSavedCodeAtom.ts
  • apps/cowswap-frontend/src/modules/affiliate/state/migrations/affiliateTraderSavedCodeStorage.utils.ts
  • apps/cowswap-frontend/src/pages/Account/AffiliatePartner.tsx
  • apps/cowswap-frontend/src/pages/Account/AffiliateTrader.tsx

Comment on lines +62 to +81
export function getAffiliateTraderPageState(
walletStatus: TraderWalletStatus,
hasSavedCode: boolean,
): AffiliatePageState {
if (hasSavedCode && walletStatus !== TraderWalletStatus.DISCONNECTED) {
return AffiliatePageState.LINKED
}

switch (walletStatus) {
case TraderWalletStatus.PENDING:
return AffiliatePageState.LOADING
case TraderWalletStatus.UNSUPPORTED:
return AffiliatePageState.UNSUPPORTED
case TraderWalletStatus.INELIGIBLE:
return AffiliatePageState.INELIGIBLE
case TraderWalletStatus.ELIGIBILITY_UNKNOWN:
return AffiliatePageState.ELIGIBILITY_UNKNOWN
default:
return AffiliatePageState.ONBOARD
}
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.

⚠️ Potential issue | 🟠 Major

Handle TraderWalletStatus.LINKED explicitly to avoid misclassified page-state analytics.

On Line 66, LINKED depends on hasSavedCode. If wallet status is already LINKED but local saved code is absent (e.g., before/while recovery), this falls through to ONBOARD, which emits incorrect funnel state.

💡 Suggested fix
 export function getAffiliateTraderPageState(
   walletStatus: TraderWalletStatus,
   hasSavedCode: boolean,
 ): AffiliatePageState {
-  if (hasSavedCode && walletStatus !== TraderWalletStatus.DISCONNECTED) {
+  if (walletStatus === TraderWalletStatus.LINKED) {
+    return AffiliatePageState.LINKED
+  }
+
+  if (hasSavedCode && walletStatus !== TraderWalletStatus.DISCONNECTED) {
     return AffiliatePageState.LINKED
   }

   switch (walletStatus) {
     case TraderWalletStatus.PENDING:
       return AffiliatePageState.LOADING
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getAffiliateTraderPageState(
walletStatus: TraderWalletStatus,
hasSavedCode: boolean,
): AffiliatePageState {
if (hasSavedCode && walletStatus !== TraderWalletStatus.DISCONNECTED) {
return AffiliatePageState.LINKED
}
switch (walletStatus) {
case TraderWalletStatus.PENDING:
return AffiliatePageState.LOADING
case TraderWalletStatus.UNSUPPORTED:
return AffiliatePageState.UNSUPPORTED
case TraderWalletStatus.INELIGIBLE:
return AffiliatePageState.INELIGIBLE
case TraderWalletStatus.ELIGIBILITY_UNKNOWN:
return AffiliatePageState.ELIGIBILITY_UNKNOWN
default:
return AffiliatePageState.ONBOARD
}
export function getAffiliateTraderPageState(
walletStatus: TraderWalletStatus,
hasSavedCode: boolean,
): AffiliatePageState {
if (walletStatus === TraderWalletStatus.LINKED) {
return AffiliatePageState.LINKED
}
if (hasSavedCode && walletStatus !== TraderWalletStatus.DISCONNECTED) {
return AffiliatePageState.LINKED
}
switch (walletStatus) {
case TraderWalletStatus.PENDING:
return AffiliatePageState.LOADING
case TraderWalletStatus.UNSUPPORTED:
return AffiliatePageState.UNSUPPORTED
case TraderWalletStatus.INELIGIBLE:
return AffiliatePageState.INELIGIBLE
case TraderWalletStatus.ELIGIBILITY_UNKNOWN:
return AffiliatePageState.ELIGIBILITY_UNKNOWN
default:
return AffiliatePageState.ONBOARD
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.utils.ts`
around lines 62 - 81, The function getAffiliateTraderPageState currently sets
LINKED only when hasSavedCode is true, causing a walletStatus of
TraderWalletStatus.LINKED to be misclassified as ONBOARD if hasSavedCode is
false; update getAffiliateTraderPageState to explicitly check for
TraderWalletStatus.LINKED and return AffiliatePageState.LINKED (independent of
hasSavedCode) before the final default/other switches so the page-state
correctly reflects the wallet's LINKED status.

Comment thread apps/cowswap-frontend/src/pages/Account/AffiliateTrader.tsx Outdated
Copy link
Copy Markdown
Contributor

@elena-zh elena-zh left a comment

Choose a reason for hiding this comment

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

Hey @fairlighteth , looks good! Maybe 2 nitpicks:

  1. When I create an affiliate code, I see 2 events fired one after another: code creation completed, then another the same one with the reason 'code is unavailable'. Is it OK?
Image
  1. I did not notice that there is an event for an expired code: should we add it? Or if it exists, let me know please a name of the event I should check.
Image

Thanks!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.ts (1)

46-57: ⚠️ Potential issue | 🟠 Major

Move pre-submit side effects inside the try to guarantee guard cleanup.

isSubmittingRef.current is set before entering try, but analytics/state calls run outside it. If one of those throws, cleanup never runs and submissions stay locked for this component lifetime.

🔧 Suggested fix
   const onCreate = useCallback(async (): Promise<void> => {
     if (!account || !provider || isSubmittingRef.current) return

     isSubmittingRef.current = true
-
-    trackAffiliateEvent({
-      analytics,
-      action: 'affiliate_partner_code_create_started',
-      chainId: AFFILIATE_PAYOUTS_CHAIN_ID,
-    })
-
-    setSubmitting(true)
-    setError(undefined)
-
     try {
+      trackAffiliateEvent({
+        analytics,
+        action: 'affiliate_partner_code_create_started',
+        chainId: AFFILIATE_PAYOUTS_CHAIN_ID,
+      })
+      setSubmitting(true)
+      setError(undefined)
+
       const signer = provider.getSigner()
       const typedData = buildPartnerTypedData({
         walletAddress: account,
         code,
         chainId: AFFILIATE_PAYOUTS_CHAIN_ID,

Also applies to: 90-92

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.ts`
around lines 46 - 57, Move the pre-submit side effects (setting
isSubmittingRef.current = true, calling trackAffiliateEvent,
setSubmitting(true), setError(undefined)) into the try block inside
useAffiliatePartnerCodeCreate so any exception thrown by analytics/state updates
is caught and the finally cleanup runs; ensure the same rearrangement is applied
to the duplicate block around the code referenced at lines 90-92, and keep the
finally block that resets isSubmittingRef.current and setSubmitting(false)
unchanged so submissions are never left locked.
🧹 Nitpick comments (1)
apps/cowswap-frontend/src/modules/affiliate/state/migrations/affiliateTraderSavedCodeStorage.utils.ts (1)

26-34: Add a removal TODO for the legacy migration.

The migration from v0 to v1 storage should include a cleanup TODO with a target removal date. This helps track when the migration code can be safely removed after sufficient time has passed for users to migrate.

As per coding guidelines: "Tag migrations with a removal // TODO date for cleanup"

📅 Proposed TODO annotation
     const legacyValue = readSavedCodeState(AFFILIATE_TRADER_SAVED_CODES_LEGACY_STORAGE_KEY)
 
     if (!legacyValue) {
       return initialValue
     }
 
+    // TODO(2026-07-15): Remove v0 → v1 migration after 3 months
     localStorage.setItem(AFFILIATE_TRADER_SAVED_CODES_STORAGE_KEY, JSON.stringify(legacyValue))
     localStorage.removeItem(AFFILIATE_TRADER_SAVED_CODES_LEGACY_STORAGE_KEY)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/cowswap-frontend/src/modules/affiliate/state/migrations/affiliateTraderSavedCodeStorage.utils.ts`
around lines 26 - 34, Add a one-line TODO above the legacy migration that
documents when the migration code can be removed (e.g., target removal date), so
future cleanup is tracked; specifically, update the block that reads from
readSavedCodeState(AFFILIATE_TRADER_SAVED_CODES_LEGACY_STORAGE_KEY) and writes
to AFFILIATE_TRADER_SAVED_CODES_STORAGE_KEY to include a comment like "// TODO:
remove migration after YYYY-MM" to mark the planned removal date for the legacy
handling of AFFILIATE_TRADER_SAVED_CODES_LEGACY_STORAGE_KEY.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderModalCodeLinking.tsx`:
- Around line 64-75: The onTogglePayoutConfirmed callback is sending analytics
without the modal context fields; update the trackAffiliateEvent call inside
onTogglePayoutConfirmed to include entrySource and walletStatus (or the
equivalent variables used elsewhere in this file) so the payout-confirmation
event is segmented consistently with the trader modal funnel, and add those
variables to the useCallback dependency array; locate the
onTogglePayoutConfirmed function and the trackAffiliateEvent invocation to apply
this change.

In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.test.tsx`:
- Around line 40-41: The createDeferred helper currently declares let resolve!
and let reject! using non-null assertion; remove the definite-assignment
assertions and instead initialize resolve and reject with safe defaults that
throw if called before being set (e.g., functions that throw an explicit
"deferred not initialized" error), preserving their types (resolve: (value: T)
=> void, reject: (error?: unknown) => void) and then reassign them when the real
Promise executor runs; update any type annotations in createDeferred so no
non-null assertions are used.

In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliateStateViewAnalytics.ts`:
- Line 10: The eventParams type and usage in useAffiliateStateViewAnalytics
currently allow reserved keys ('action' and 'analytics') to be passed and
overwrite analytics fields; change the param type to enforce those keys can't be
provided (e.g., eventParams?: Record<string, unknown> & { action?: never;
analytics?: never }) and before merging/spreading into the final payload
sanitize the runtime object (e.g., create sanitizedParams from eventParams and
delete sanitizedParams.action and sanitizedParams.analytics) so reserved fields
cannot be overridden when constructing the analytics event.

---

Outside diff comments:
In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.ts`:
- Around line 46-57: Move the pre-submit side effects (setting
isSubmittingRef.current = true, calling trackAffiliateEvent,
setSubmitting(true), setError(undefined)) into the try block inside
useAffiliatePartnerCodeCreate so any exception thrown by analytics/state updates
is caught and the finally cleanup runs; ensure the same rearrangement is applied
to the duplicate block around the code referenced at lines 90-92, and keep the
finally block that resets isSubmittingRef.current and setSubmitting(false)
unchanged so submissions are never left locked.

---

Nitpick comments:
In
`@apps/cowswap-frontend/src/modules/affiliate/state/migrations/affiliateTraderSavedCodeStorage.utils.ts`:
- Around line 26-34: Add a one-line TODO above the legacy migration that
documents when the migration code can be removed (e.g., target removal date), so
future cleanup is tracked; specifically, update the block that reads from
readSavedCodeState(AFFILIATE_TRADER_SAVED_CODES_LEGACY_STORAGE_KEY) and writes
to AFFILIATE_TRADER_SAVED_CODES_STORAGE_KEY to include a comment like "// TODO:
remove migration after YYYY-MM" to mark the planned removal date for the legacy
handling of AFFILIATE_TRADER_SAVED_CODES_LEGACY_STORAGE_KEY.
🪄 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: eec813d1-bf48-492a-80be-dcf1d99ac397

📥 Commits

Reviewing files that changed from the base of the PR and between 6d41584 and 15f4d39.

📒 Files selected for processing (7)
  • apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.test.ts
  • apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderModalCodeLinking.tsx
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.test.tsx
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.ts
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliateStateViewAnalytics.ts
  • apps/cowswap-frontend/src/modules/affiliate/state/migrations/affiliateTraderSavedCodeStorage.utils.ts
  • apps/cowswap-frontend/src/pages/Account/AffiliateTrader.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/cowswap-frontend/src/pages/Account/AffiliateTrader.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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 (2)
apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderExpiryBanner.test.tsx (1)

117-131: Add a boundary test for rewards_end === now.

Current tests cover before/after; adding exact-equality coverage would protect against expiry-state edge regressions in both banner rendering and analytics payload selection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderExpiryBanner.test.tsx`
around lines 117 - 131, Add a new unit test in
AffiliateTraderExpiryBanner.test.tsx that mocks useAffiliateTraderStatsMock (via
createTraderStatsResponse) with rewards_end exactly equal to the mocked machine
time from useMachineTimeMsMock (Date.parse(...)), calls renderComponent(), and
asserts the banner/analytics treat this as the expiry boundary: specifically
verify useAffiliateStateViewAnalyticsMock is invoked with the expired payload
(action: 'affiliate_trader_expired_code_viewed') and that the component renders
the expired-banner state accordingly; locate mocks and helpers by names
useAffiliateTraderStatsMock, createTraderStatsResponse, useMachineTimeMsMock,
renderComponent, and useAffiliateStateViewAnalyticsMock.
apps/cowswap-frontend/src/modules/affiliate/hooks/useIsRefCodeExpired.test.tsx (1)

91-102: Test intent and assertions are slightly misaligned.

The title says “tracking is enabled”, but this case only asserts expiry state. Consider either renaming the test or asserting tracking behavior explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useIsRefCodeExpired.test.tsx`
around lines 91 - 102, The test title and assertions are misaligned; either make
the title reflect that this test only asserts expiry (e.g., rename the it(...)
string to "still reports expired codes") or add an explicit assertion that
verifies tracking is enabled (ensure useAffiliateTraderStatsMock is called with
the tracking flag true). Locate the test in useIsRefCodeExpired.test.tsx and
update the it(...) description or add/check the assertion that
useAffiliateTraderStatsMock was called with 'true' (the call asserted against in
this file) alongside the existing expiry assertion for result.current from
useIsRefCodeExpired.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderExpiryBanner.tsx`:
- Around line 25-29: The logic treating the exact expiry moment as neither
expired nor pre-expiry causes the banner to vanish briefly; change the expiry
comparison so rewardsEndTimestamp is considered expired when equal to now by
updating isExpired to use <= (i.e. rewardsEndTimestamp <= now) and keep
isPreExpiry using strict > now with the PRE_EXPIRY_WINDOW_MS check so the exact
boundary falls into the expired branch; update the boolean definitions for
rewardsEndTimestamp, isExpired, and isPreExpiry accordingly.

---

Nitpick comments:
In
`@apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderExpiryBanner.test.tsx`:
- Around line 117-131: Add a new unit test in
AffiliateTraderExpiryBanner.test.tsx that mocks useAffiliateTraderStatsMock (via
createTraderStatsResponse) with rewards_end exactly equal to the mocked machine
time from useMachineTimeMsMock (Date.parse(...)), calls renderComponent(), and
asserts the banner/analytics treat this as the expiry boundary: specifically
verify useAffiliateStateViewAnalyticsMock is invoked with the expired payload
(action: 'affiliate_trader_expired_code_viewed') and that the component renders
the expired-banner state accordingly; locate mocks and helpers by names
useAffiliateTraderStatsMock, createTraderStatsResponse, useMachineTimeMsMock,
renderComponent, and useAffiliateStateViewAnalyticsMock.

In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useIsRefCodeExpired.test.tsx`:
- Around line 91-102: The test title and assertions are misaligned; either make
the title reflect that this test only asserts expiry (e.g., rename the it(...)
string to "still reports expired codes") or add an explicit assertion that
verifies tracking is enabled (ensure useAffiliateTraderStatsMock is called with
the tracking flag true). Locate the test in useIsRefCodeExpired.test.tsx and
update the it(...) description or add/check the assertion that
useAffiliateTraderStatsMock was called with 'true' (the call asserted against in
this file) alongside the existing expiry assertion for result.current from
useIsRefCodeExpired.
🪄 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: 903806f3-2716-46ba-8892-eb44f74b1a4f

📥 Commits

Reviewing files that changed from the base of the PR and between 15f4d39 and d337723.

📒 Files selected for processing (5)
  • apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.types.ts
  • apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderExpiryBanner.test.tsx
  • apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderExpiryBanner.tsx
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.test.tsx
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useIsRefCodeExpired.test.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.types.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.ts`:
- Around line 49-53: The telemetry calls to trackAffiliateEvent (the start and
completion events around createCode and in onCreate) must be made best-effort so
they cannot block or cause createCode/onCreate to fail; wrap each
trackAffiliateEvent invocation in a try/catch (or otherwise swallow its errors)
and do not allow thrown errors to propagate to the caller nor to change the
return/throw behavior of createCode or onCreate; ensure any awaited telemetry is
moved inside these try/catch blocks or made non-blocking so the affiliate API
call and its success/failure flow remain independent (apply the same change for
the calls referenced around the start block and the completion/error block in
the createCode/onCreate flow).
🪄 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: 81ed4901-7f4a-4096-b4ce-3d1770acf9f1

📥 Commits

Reviewing files that changed from the base of the PR and between d337723 and 1ceee40.

📒 Files selected for processing (6)
  • apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.test.ts
  • apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderModalCodeLinking.tsx
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.test.tsx
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.ts
  • apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliateStateViewAnalytics.ts
  • apps/cowswap-frontend/src/modules/affiliate/state/migrations/affiliateTraderSavedCodeStorage.utils.ts

Comment on lines +49 to +53
trackAffiliateEvent({
analytics,
action: 'affiliate_partner_code_create_started',
chainId: AFFILIATE_PAYOUTS_CHAIN_ID,
})
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.

⚠️ Potential issue | 🔴 Critical

Don't let analytics failures block partner-code creation.

trackAffiliateEvent() is on the main control path here. If analytics throws on the start or completion event, createCode() is skipped or onCreate() rejects, and the user gets a misleading NetworkError even though the affiliate API may be fine. Telemetry should be best-effort around this flow, not part of the transaction boundary.

♻️ Suggested hardening
+function safeTrackAffiliateEvent(params: Parameters<typeof trackAffiliateEvent>[0]): void {
+  try {
+    trackAffiliateEvent(params)
+  } catch {
+    // optionally report via the centralized logger, but never block code creation
+  }
+}
+
 export function useAffiliatePartnerCodeCreate({
   account,
   provider,
   code,
   setError,
 }: UseAffiliatePartnerCodeCreateParams): UseAffiliatePartnerCodeCreateResult {
@@
-      trackAffiliateEvent({
+      safeTrackAffiliateEvent({
         analytics,
         action: 'affiliate_partner_code_create_started',
         chainId: AFFILIATE_PAYOUTS_CHAIN_ID,
       })
@@
-      trackAffiliateEvent({
+      safeTrackAffiliateEvent({
         analytics,
         action: 'affiliate_partner_code_create_completed',
         chainId: AFFILIATE_PAYOUTS_CHAIN_ID,
         result: 'success',
       })
@@
-      trackAffiliateEvent({
+      safeTrackAffiliateEvent({
         analytics,
         action: 'affiliate_partner_code_create_completed',
         chainId: AFFILIATE_PAYOUTS_CHAIN_ID,
         result: 'failed',
         failureReason: normalizeAffiliatePartnerCodeCreateFailureReason(mappedError),
       })

Also applies to: 72-89

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.ts`
around lines 49 - 53, The telemetry calls to trackAffiliateEvent (the start and
completion events around createCode and in onCreate) must be made best-effort so
they cannot block or cause createCode/onCreate to fail; wrap each
trackAffiliateEvent invocation in a try/catch (or otherwise swallow its errors)
and do not allow thrown errors to propagate to the caller nor to change the
return/throw behavior of createCode or onCreate; ensure any awaited telemetry is
moved inside these try/catch blocks or made non-blocking so the affiliate API
call and its success/failure flow remain independent (apply the same change for
the calls referenced around the start block and the completion/error block in
the createCode/onCreate flow).

@fairlighteth
Copy link
Copy Markdown
Contributor Author

@elena-zh thanks for reviewing!

I checked the exact flow in GTM Preview, and the emitted events look correct on my side. I see affiliate_partner_code_create_started followed by a single affiliate_partner_code_create_completed, plus the expected suggestion/availability/page-view events. I don’t see a second completion event with codeUnavailable.

My guess is your screenshot may have been from debug logs, but in the current branch the actual emitted GTM events for this flow look fine (console events is not the same as actual emitted GTM events).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.utils.ts (1)

71-86: ⚠️ Potential issue | 🟠 Major

Handle TraderWalletStatus.LINKED explicitly in page-state mapping.

On Line 85, a LINKED wallet can still resolve to ONBOARD when hasSavedCode is false, which misclassifies trader funnel analytics. Add an explicit LINKED case before fallback logic.

💡 Suggested fix
 export function getAffiliateTraderPageState(
   walletStatus: TraderWalletStatus,
   hasSavedCode: boolean,
 ): AffiliatePageState {
+  if (walletStatus === TraderWalletStatus.LINKED) {
+    return AffiliatePageState.LINKED
+  }
+
   switch (walletStatus) {
     case TraderWalletStatus.PENDING:
       return AffiliatePageState.LOADING
     case TraderWalletStatus.UNSUPPORTED:
       return AffiliatePageState.UNSUPPORTED
     case TraderWalletStatus.INELIGIBLE:
       return AffiliatePageState.INELIGIBLE
     case TraderWalletStatus.DISCONNECTED:
       return AffiliatePageState.ONBOARD
     default:
       return hasSavedCode ? AffiliatePageState.LINKED : AffiliatePageState.ONBOARD
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.utils.ts`
around lines 71 - 86, The mapping in getAffiliateTraderPageState incorrectly
lets TraderWalletStatus.LINKED fall through to the fallback that uses
hasSavedCode, potentially returning ONBOARD; add an explicit case for
TraderWalletStatus.LINKED that returns AffiliatePageState.LINKED before the
default/fallback so LINKED wallets are always classified as
AffiliatePageState.LINKED regardless of hasSavedCode.
🧹 Nitpick comments (1)
apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.test.tsx (1)

128-158: Assert success completion analytics in the “start event throws” path.

This test proves creation continues, but it doesn’t verify that affiliate_partner_code_create_completed with result: 'success' is still emitted afterward.

♻️ Suggested assertion addition
   await act(async () => {
     await result.current.onCreate()
   })

   expect(provider.getSigner).toHaveBeenCalledTimes(1)
   expect(createCodeMock).toHaveBeenCalledTimes(1)
   expect(mutatePartnerInfo).toHaveBeenCalledTimes(1)
   expect(setError.mock.calls).toEqual([[undefined]])
+
+  const trackedEvents = sendEvent.mock.calls.map(([event]) => event)
+  const completedEvents = trackedEvents.filter((event) => event.action === 'affiliate_partner_code_create_completed')
+  expect(completedEvents).toHaveLength(1)
+  expect(completedEvents[0]).toMatchObject({
+    category: CowSwapAnalyticsCategory.AFFILIATE,
+    action: 'affiliate_partner_code_create_completed',
+    chainId: AFFILIATE_PAYOUTS_CHAIN_ID,
+    result: 'success',
+  })

Based on learnings: "Add additional Jest logic specs or Storybook/Cosmos visual checks as behavior demands."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.test.tsx`
around lines 128 - 158, The test for useAffiliatePartnerCodeCreate that
simulates sendEvent throwing should also assert a success completion analytic is
still emitted: after calling result.current.onCreate(), add an assertion that
sendEvent was called with the 'affiliate_partner_code_create_completed' event
and payload containing result: 'success' (and relevant identifiers like
code/account if used), while keeping the existing assertions for
provider.getSigner, createCodeMock, mutatePartnerInfo and setError; locate
useAffiliatePartnerCodeCreate, the test's sendEvent mock, and the
'affiliate_partner_code_create_completed' event name to update the expectations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.utils.ts`:
- Around line 71-86: The mapping in getAffiliateTraderPageState incorrectly lets
TraderWalletStatus.LINKED fall through to the fallback that uses hasSavedCode,
potentially returning ONBOARD; add an explicit case for
TraderWalletStatus.LINKED that returns AffiliatePageState.LINKED before the
default/fallback so LINKED wallets are always classified as
AffiliatePageState.LINKED regardless of hasSavedCode.

---

Nitpick comments:
In
`@apps/cowswap-frontend/src/modules/affiliate/hooks/useAffiliatePartnerCodeCreate.test.tsx`:
- Around line 128-158: The test for useAffiliatePartnerCodeCreate that simulates
sendEvent throwing should also assert a success completion analytic is still
emitted: after calling result.current.onCreate(), add an assertion that
sendEvent was called with the 'affiliate_partner_code_create_completed' event
and payload containing result: 'success' (and relevant identifiers like
code/account if used), while keeping the existing assertions for
provider.getSigner, createCodeMock, mutatePartnerInfo and setError; locate
useAffiliatePartnerCodeCreate, the test's sendEvent mock, and the
'affiliate_partner_code_create_completed' event name to update the expectations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 49e43748-8c5b-4073-8522-e87d431db089

📥 Commits

Reviewing files that changed from the base of the PR and between 1ceee40 and 823bc9d.

📒 Files selected for processing (3)
  • 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
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/cowswap-frontend/src/modules/affiliate/analytics/affiliateAnalytics.test.ts

@elena-zh
Copy link
Copy Markdown
Contributor

@elena-zh thanks for reviewing!

I checked the exact flow in GTM Preview, and the emitted events look correct on my side. I see affiliate_partner_code_create_started followed by a single affiliate_partner_code_create_completed, plus the expected suggestion/availability/page-view events. I don’t see a second completion event with codeUnavailable.

My guess is your screenshot may have been from debug logs, but in the current branch the actual emitted GTM events for this flow look fine (console events is not the same as actual emitted GTM events).

Hey @fairlighteth , thank you for taking a look at it.
Could you please take a look at my 2nd question regarding to 'expired affiliate code' handling? #7303 (review)

@fairlighteth
Copy link
Copy Markdown
Contributor Author

@elena-zh Yes, there is now an event for that: affiliate_trader_expired_code_viewed and it fires when the code is in the expired state.

Copy link
Copy Markdown
Contributor

@elena-zh elena-zh left a comment

Choose a reason for hiding this comment

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

thank you!

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.

2 participants