From 1a1c0de7ffd1f97bc7841b5ea2e8d5a22e1682ae Mon Sep 17 00:00:00 2001 From: glary-bot Date: Sat, 16 May 2026 09:10:05 +0000 Subject: [PATCH] feat(telemetry): capture Rewardful referral on checkout attribution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the existing Impact wiring for the new Rewardful affiliate network: client reads window.Rewardful.referral when getCheckoutAttribution runs, and emits it as a new optional rewardful_referral field on CheckoutAttributionMetadata. The Go backend (comfy-api) consumes this field separately and passes it to Stripe as ClientReferenceID on the CheckoutSession create call — that wiring lives in a sibling PR on Comfy-Org/comfy-api and is the path that actually credits affiliate commissions for Stripe subscriptions (per Rewardful docs, GTM-loaded JS alone cannot attribute Checkout Sessions; the merchant must pass the referral UUID server-side). Why this is the simplest possible client-side change: - Rewardful's JS (loaded via GTM) owns its own cookie persistence, so unlike Impact (where we capture im_ref from URL params and persist to localStorage ourselves) we just read window.Rewardful.referral at checkout time. No URL fallback, no localStorage handling. If Rewardful's script hasn't loaded or the user didn't come from an affiliate link, the field is omitted from the payload entirely. - Adds a narrow RewardfulGlobal interface to global.d.ts (referral plus optional affiliate/campaign metadata Rewardful exposes) so window.Rewardful is typed everywhere. - Adds 4 unit tests covering: present, absent, empty-string, and alongside Impact attribution. The existing 10 Impact/UTM tests are untouched. Verified (locally on the workspace clone): - pnpm typecheck — clean - pnpm test:unit src/platform/telemetry/utils — 14/14 (10 prior + 4 new) - pnpm test:unit (full repo) — passing - pnpm lint — 3 warnings, 0 errors (warnings pre-existing on main) - pnpm format:check — clean - pnpm knip — clean (1 pre-existing warning unrelated) - pnpm exec vite build — successful (7.85s) Cross-PR dependency: needs the sibling comfy-api PR to actually credit referrals. This PR is safe to ship independently — the field is just ignored by the existing comfy-api endpoint until that PR lands. --- global.d.ts | 7 +++ src/platform/telemetry/types.ts | 1 + .../__tests__/checkoutAttribution.test.ts | 44 +++++++++++++++++++ .../telemetry/utils/checkoutAttribution.ts | 9 +++- 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/global.d.ts b/global.d.ts index e0a154c311..b46bc54180 100644 --- a/global.d.ts +++ b/global.d.ts @@ -11,6 +11,12 @@ interface ImpactQueueFunction { a?: unknown[][] } +interface RewardfulGlobal { + referral?: string + affiliate?: { id?: string; token?: string; name?: string } + campaign?: { id?: string; name?: string } +} + type GtagGetFieldName = 'client_id' | 'session_id' | 'session_number' interface GtagGetFieldValueMap { @@ -63,6 +69,7 @@ interface Window { gtag?: GtagFunction ire_o?: string ire?: ImpactQueueFunction + Rewardful?: RewardfulGlobal } interface Navigator { diff --git a/src/platform/telemetry/types.ts b/src/platform/telemetry/types.ts index e606379eb5..71d47c3607 100644 --- a/src/platform/telemetry/types.ts +++ b/src/platform/telemetry/types.ts @@ -325,6 +325,7 @@ export interface CheckoutAttributionMetadata { ga_session_id?: string ga_session_number?: string im_ref?: string + rewardful_referral?: string utm_source?: string utm_medium?: string utm_campaign?: string diff --git a/src/platform/telemetry/utils/__tests__/checkoutAttribution.test.ts b/src/platform/telemetry/utils/__tests__/checkoutAttribution.test.ts index a17522c044..79a6792833 100644 --- a/src/platform/telemetry/utils/__tests__/checkoutAttribution.test.ts +++ b/src/platform/telemetry/utils/__tests__/checkoutAttribution.test.ts @@ -15,6 +15,7 @@ describe('getCheckoutAttribution', () => { } window.gtag = undefined window.ire = undefined + window.Rewardful = undefined window.history.pushState({}, '', '/') }) @@ -228,4 +229,47 @@ describe('getCheckoutAttribution', () => { expect(attribution.im_ref).toBeUndefined() }) + + it('captures Rewardful referral from window.Rewardful', async () => { + window.Rewardful = { + referral: 'rwd-abc-123' + } + + const attribution = await getCheckoutAttribution() + + expect(attribution.rewardful_referral).toBe('rwd-abc-123') + }) + + it('returns undefined Rewardful referral when window.Rewardful is absent', async () => { + const attribution = await getCheckoutAttribution() + + expect(attribution.rewardful_referral).toBeUndefined() + }) + + it('returns undefined Rewardful referral when window.Rewardful.referral is empty', async () => { + window.Rewardful = { referral: '' } + + const attribution = await getCheckoutAttribution() + + expect(attribution.rewardful_referral).toBeUndefined() + }) + + it('captures Rewardful referral alongside Impact attribution', async () => { + window.history.pushState( + {}, + '', + '/?im_ref=impact-url-id&utm_source=affiliate' + ) + window.Rewardful = { + referral: 'rwd-xyz-789' + } + + const attribution = await getCheckoutAttribution() + + expect(attribution).toMatchObject({ + im_ref: 'impact-url-id', + utm_source: 'affiliate', + rewardful_referral: 'rwd-xyz-789' + }) + }) }) diff --git a/src/platform/telemetry/utils/checkoutAttribution.ts b/src/platform/telemetry/utils/checkoutAttribution.ts index c525a35a10..fd9b5640ae 100644 --- a/src/platform/telemetry/utils/checkoutAttribution.ts +++ b/src/platform/telemetry/utils/checkoutAttribution.ts @@ -180,6 +180,11 @@ async function getGeneratedClickId(): Promise { } } +function getRewardfulReferral(): string | undefined { + if (typeof window === 'undefined') return undefined + return asNonEmptyString(window.Rewardful?.referral) +} + export function captureCheckoutAttributionFromSearch(search: string): void { const fromUrl = readAttributionFromUrl(search) const storedAttribution = readStoredAttribution() @@ -213,11 +218,13 @@ export async function getCheckoutAttribution(): Promise