mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-04 04:13:46 +00:00
Compare commits
2 Commits
cloud/v1.4
...
cloud/1.44
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fa7e25053 | ||
|
|
9035f66df3 |
14
global.d.ts
vendored
14
global.d.ts
vendored
@@ -11,6 +11,18 @@ interface ImpactQueueFunction {
|
||||
a?: unknown[][]
|
||||
}
|
||||
|
||||
interface RewardfulGlobal {
|
||||
referral?: string
|
||||
affiliate?: { id?: string; token?: string; name?: string }
|
||||
campaign?: { id?: string; name?: string }
|
||||
}
|
||||
|
||||
interface RewardfulQueueFunction {
|
||||
(method: 'ready', callback: () => void): void
|
||||
(...args: unknown[]): void
|
||||
q?: unknown[][]
|
||||
}
|
||||
|
||||
type GtagGetFieldName = 'client_id' | 'session_id' | 'session_number'
|
||||
|
||||
interface GtagGetFieldValueMap {
|
||||
@@ -63,6 +75,8 @@ interface Window {
|
||||
gtag?: GtagFunction
|
||||
ire_o?: string
|
||||
ire?: ImpactQueueFunction
|
||||
rewardful?: RewardfulQueueFunction
|
||||
Rewardful?: RewardfulGlobal
|
||||
}
|
||||
|
||||
interface Navigator {
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('PostHogTelemetryProvider', () => {
|
||||
expect(hoisted.mockOnUserResolved).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('identifies user when onUserResolved fires', async () => {
|
||||
it('identifies user without setting first_auth_at when onUserResolved fires', async () => {
|
||||
createProvider()
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
@@ -172,6 +172,88 @@ describe('PostHogTelemetryProvider', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('sets first_auth_at on new-user auth', async () => {
|
||||
const provider = createProvider()
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
provider.trackAuth({
|
||||
method: 'google',
|
||||
is_new_user: true,
|
||||
user_id: 'user-123'
|
||||
})
|
||||
|
||||
expect(hoisted.mockIdentify).toHaveBeenCalledWith(
|
||||
'user-123',
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
first_auth_at: expect.any(String)
|
||||
})
|
||||
)
|
||||
expect(hoisted.mockCapture).toHaveBeenCalledWith(
|
||||
TelemetryEvents.USER_AUTH_COMPLETED,
|
||||
{
|
||||
method: 'google',
|
||||
is_new_user: true,
|
||||
user_id: 'user-123'
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('does not set first_auth_at on returning-user auth', async () => {
|
||||
const provider = createProvider()
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
provider.trackAuth({
|
||||
method: 'google',
|
||||
is_new_user: false,
|
||||
user_id: 'user-123'
|
||||
})
|
||||
|
||||
expect(hoisted.mockIdentify).not.toHaveBeenCalled()
|
||||
expect(hoisted.mockCapture).toHaveBeenCalledWith(
|
||||
TelemetryEvents.USER_AUTH_COMPLETED,
|
||||
{
|
||||
method: 'google',
|
||||
is_new_user: false,
|
||||
user_id: 'user-123'
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('flushes queued first_auth_at before queued auth event', async () => {
|
||||
const provider = createProvider()
|
||||
|
||||
provider.trackAuth({
|
||||
method: 'google',
|
||||
is_new_user: true,
|
||||
user_id: 'user-123'
|
||||
})
|
||||
|
||||
expect(hoisted.mockIdentify).not.toHaveBeenCalled()
|
||||
expect(hoisted.mockCapture).not.toHaveBeenCalled()
|
||||
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(hoisted.mockIdentify).toHaveBeenCalledWith(
|
||||
'user-123',
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
first_auth_at: expect.any(String)
|
||||
})
|
||||
)
|
||||
expect(hoisted.mockCapture).toHaveBeenCalledWith(
|
||||
TelemetryEvents.USER_AUTH_COMPLETED,
|
||||
{
|
||||
method: 'google',
|
||||
is_new_user: true,
|
||||
user_id: 'user-123'
|
||||
}
|
||||
)
|
||||
expect(hoisted.mockIdentify.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
hoisted.mockCapture.mock.invocationCallOrder[0]
|
||||
)
|
||||
})
|
||||
|
||||
it('queues events before initialization and flushes after', async () => {
|
||||
const provider = createProvider()
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ export class PostHogTelemetryProvider implements TelemetryProvider {
|
||||
private isEnabled = true
|
||||
private posthog: PostHog | null = null
|
||||
private eventQueue: QueuedEvent[] = []
|
||||
private pendingFirstAuthAt = new Map<string, string>()
|
||||
private isInitialized = false
|
||||
private lastTriggerSource: ExecutionTriggerSource | undefined
|
||||
private disabledEvents = new Set<TelemetryEventName>(DEFAULT_DISABLED_EVENTS)
|
||||
@@ -164,6 +165,8 @@ export class PostHogTelemetryProvider implements TelemetryProvider {
|
||||
private flushEventQueue(): void {
|
||||
if (!this.isInitialized || !this.posthog) return
|
||||
|
||||
this.flushPendingFirstAuthAt()
|
||||
|
||||
while (this.eventQueue.length > 0) {
|
||||
const event = this.eventQueue.shift()!
|
||||
try {
|
||||
@@ -174,6 +177,33 @@ export class PostHogTelemetryProvider implements TelemetryProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private flushPendingFirstAuthAt(): void {
|
||||
for (const [userId, firstAuthAt] of this.pendingFirstAuthAt) {
|
||||
this.setFirstAuthAt(userId, firstAuthAt)
|
||||
}
|
||||
this.pendingFirstAuthAt.clear()
|
||||
}
|
||||
|
||||
private setFirstAuthAt(
|
||||
userId: string,
|
||||
firstAuthAt = new Date().toISOString()
|
||||
): void {
|
||||
if (!this.isEnabled) return
|
||||
|
||||
if (this.isInitialized && this.posthog) {
|
||||
try {
|
||||
this.posthog.identify(userId, undefined, { first_auth_at: firstAuthAt })
|
||||
} catch (error) {
|
||||
console.error('Failed to set PostHog first auth timestamp:', error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.pendingFirstAuthAt.has(userId)) {
|
||||
this.pendingFirstAuthAt.set(userId, firstAuthAt)
|
||||
}
|
||||
}
|
||||
|
||||
private trackEvent(
|
||||
eventName: TelemetryEventName,
|
||||
properties?: TelemetryEventProperties
|
||||
@@ -254,6 +284,9 @@ export class PostHogTelemetryProvider implements TelemetryProvider {
|
||||
}
|
||||
|
||||
trackAuth(metadata: AuthMetadata): void {
|
||||
if (metadata.is_new_user && metadata.user_id) {
|
||||
this.setFirstAuthAt(metadata.user_id)
|
||||
}
|
||||
this.trackEvent(TelemetryEvents.USER_AUTH_COMPLETED, metadata)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
captureCheckoutAttributionFromSearch,
|
||||
@@ -15,9 +15,15 @@ describe('getCheckoutAttribution', () => {
|
||||
}
|
||||
window.gtag = undefined
|
||||
window.ire = undefined
|
||||
window.rewardful = undefined
|
||||
window.Rewardful = undefined
|
||||
window.history.pushState({}, '', '/')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('reads GA identity and URL attribution, and prefers generated click id', async () => {
|
||||
window.__CONFIG__ = {
|
||||
...window.__CONFIG__,
|
||||
@@ -228,4 +234,80 @@ 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('waits for Rewardful ready before reading the referral', async () => {
|
||||
let readyCallback: (() => void) | undefined
|
||||
window.rewardful = vi.fn((_method: 'ready', callback: () => void) => {
|
||||
readyCallback = callback
|
||||
}) as Window['rewardful']
|
||||
|
||||
const attributionPromise = getCheckoutAttribution()
|
||||
await Promise.resolve()
|
||||
|
||||
expect(window.rewardful).toHaveBeenCalledWith('ready', expect.any(Function))
|
||||
|
||||
window.Rewardful = {
|
||||
referral: 'rwd-ready-123'
|
||||
}
|
||||
readyCallback?.()
|
||||
|
||||
const attribution = await attributionPromise
|
||||
|
||||
expect(attribution.rewardful_referral).toBe('rwd-ready-123')
|
||||
})
|
||||
|
||||
it('continues checkout attribution when Rewardful ready never runs', async () => {
|
||||
vi.useFakeTimers()
|
||||
window.rewardful = vi.fn() as Window['rewardful']
|
||||
|
||||
const attributionPromise = getCheckoutAttribution()
|
||||
await vi.advanceTimersByTimeAsync(300)
|
||||
const attribution = await attributionPromise
|
||||
|
||||
expect(window.rewardful).toHaveBeenCalledWith('ready', expect.any(Function))
|
||||
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'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -31,6 +31,7 @@ type AttributionQueryKey = (typeof ATTRIBUTION_QUERY_KEYS)[number]
|
||||
const ATTRIBUTION_STORAGE_KEY = 'comfy_checkout_attribution'
|
||||
const GENERATE_CLICK_ID_TIMEOUT_MS = 300
|
||||
const GET_GA_IDENTITY_TIMEOUT_MS = 300
|
||||
const GET_REWARDFUL_REFERRAL_TIMEOUT_MS = 300
|
||||
|
||||
function readStoredAttribution(): Partial<Record<AttributionQueryKey, string>> {
|
||||
if (typeof window === 'undefined') return {}
|
||||
@@ -180,6 +181,30 @@ async function getGeneratedClickId(): Promise<string | undefined> {
|
||||
}
|
||||
}
|
||||
|
||||
async function getRewardfulReferral(): Promise<string | undefined> {
|
||||
if (typeof window === 'undefined') return undefined
|
||||
|
||||
const referral = asNonEmptyString(window.Rewardful?.referral)
|
||||
if (referral) return referral
|
||||
|
||||
const rewardful = window.rewardful
|
||||
if (typeof rewardful !== 'function') return undefined
|
||||
|
||||
return withTimeout(
|
||||
() =>
|
||||
new Promise<string | undefined>((resolve, reject) => {
|
||||
try {
|
||||
rewardful('ready', () => {
|
||||
resolve(asNonEmptyString(window.Rewardful?.referral))
|
||||
})
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
}),
|
||||
GET_REWARDFUL_REFERRAL_TIMEOUT_MS
|
||||
).catch(() => undefined)
|
||||
}
|
||||
|
||||
export function captureCheckoutAttributionFromSearch(search: string): void {
|
||||
const fromUrl = readAttributionFromUrl(search)
|
||||
const storedAttribution = readStoredAttribution()
|
||||
@@ -198,6 +223,7 @@ export async function getCheckoutAttribution(): Promise<CheckoutAttributionMetad
|
||||
|
||||
const storedAttribution = readStoredAttribution()
|
||||
const fromUrl = readAttributionFromUrl(window.location.search)
|
||||
const rewardfulReferralPromise = getRewardfulReferral()
|
||||
const generatedClickId = await getGeneratedClickId()
|
||||
const attribution: Partial<Record<AttributionQueryKey, string>> = {
|
||||
...storedAttribution,
|
||||
@@ -212,12 +238,16 @@ export async function getCheckoutAttribution(): Promise<CheckoutAttributionMetad
|
||||
persistAttribution(attribution)
|
||||
}
|
||||
|
||||
const gaIdentity = await getGaIdentity()
|
||||
const [gaIdentity, rewardfulReferral] = await Promise.all([
|
||||
getGaIdentity(),
|
||||
rewardfulReferralPromise
|
||||
])
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user