feat(telemetry): capture Rewardful referral on checkout attribution

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.
This commit is contained in:
glary-bot
2026-05-16 09:10:05 +00:00
parent 7160a9ee3f
commit 1a1c0de7ff
4 changed files with 60 additions and 1 deletions

7
global.d.ts vendored
View File

@@ -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 {

View File

@@ -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

View File

@@ -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'
})
})
})

View File

@@ -180,6 +180,11 @@ async function getGeneratedClickId(): Promise<string | undefined> {
}
}
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<CheckoutAttributionMetad
}
const gaIdentity = await getGaIdentity()
const rewardfulReferral = getRewardfulReferral()
return {
...attribution,
ga_client_id: gaIdentity?.client_id,
ga_session_id: gaIdentity?.session_id,
ga_session_number: gaIdentity?.session_number
ga_session_number: gaIdentity?.session_number,
rewardful_referral: rewardfulReferral
}
}