Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
ede4aa2
fix: route gtm through telemetry entrypoint
benceruleanlu Jan 28, 2026
c247d7b
fix: add gtm type import for knip
benceruleanlu Jan 28, 2026
563cdbf
fix: restore gtm purchase tracking
benceruleanlu Jan 28, 2026
2947a99
Merge remote-tracking branch 'origin/main' into fix/gtm-telemetry-ent…
benceruleanlu Jan 28, 2026
2db667b
fix: stabilize useSubscription tests
benceruleanlu Jan 28, 2026
074ec62
FirebaseUID gating pending purchases
benceruleanlu Jan 28, 2026
bca2c62
Add testing for telemetry in local dist assets
benceruleanlu Jan 28, 2026
a3ccd92
fix: simplify telemetry scan
benceruleanlu Jan 29, 2026
c0ee820
feat: add TelemetryRegistry for multi-provider dispatch
Jan 29, 2026
acbaf04
fix: harden telemetry dispatch
benceruleanlu Jan 29, 2026
c375b53
fix: tidy telemetry types
benceruleanlu Jan 29, 2026
f2cf5ab
fix: guard subscription purchase tracking
benceruleanlu Jan 30, 2026
a43f596
[automated] Apply ESLint and Oxfmt fixes
actions-user Jan 30, 2026
d864e7e
fix: limit gtm purchase events
benceruleanlu Jan 30, 2026
969134e
Merge branch 'fix/gtm-telemetry-entrypoint' of https://github.com/Com…
benceruleanlu Jan 30, 2026
66cee7d
fix: remove gtm id from remote config
benceruleanlu Jan 30, 2026
c439ab1
fix: isolate cloud telemetry init
benceruleanlu Jan 30, 2026
a4fce64
fix: define gtm id for cloud builds
benceruleanlu Jan 30, 2026
dd2e18d
fix: guard subscription purchase telemetry
benceruleanlu Jan 30, 2026
921c168
test: make firebase auth mock userId dynamic
benceruleanlu Jan 30, 2026
0d2dc2f
Use allowlist
benceruleanlu Jan 30, 2026
8c75f14
pin SHAs
benceruleanlu Jan 30, 2026
894fe54
chore: update ci dist telemetry action pins
benceruleanlu Jan 30, 2026
20a9514
test: fix subscription mocks
benceruleanlu Jan 30, 2026
6927c15
Fix circular import by extracting ComfyWorkflow
benceruleanlu Jan 30, 2026
22c76c7
[automated] Apply ESLint and Oxfmt fixes
actions-user Jan 30, 2026
3899450
Lazy import useDialogService
benceruleanlu Jan 30, 2026
100d0eb
Align with proper item name and item name
benceruleanlu Jan 30, 2026
50a2f16
Prefer Cloud Remote Config
benceruleanlu Jan 30, 2026
3ec99e8
fix: exclude ts files from dist scan
benceruleanlu Feb 2, 2026
ea55302
fix: rename page view tracking helper
benceruleanlu Feb 3, 2026
68944ca
fix: move auth telemetry metadata builder
benceruleanlu Feb 3, 2026
233a0c7
Rename test:dist:no-telemetry
benceruleanlu Feb 3, 2026
294675b
Update src/platform/telemetry/providers/cloud/GtmTelemetryProvider.ts
benceruleanlu Feb 3, 2026
3cd9de4
fix: use window.dataLayer directly
benceruleanlu Feb 3, 2026
d898568
Merge branch 'fix/gtm-telemetry-entrypoint' of https://github.com/Com…
benceruleanlu Feb 3, 2026
2b2043d
fix: remove duplicate dataLayer declaration
benceruleanlu Feb 3, 2026
320cafd
fix: scope dist telemetry scan permissions
benceruleanlu Feb 3, 2026
c29c6fc
test: stub crypto in auth metadata tests
benceruleanlu Feb 3, 2026
c5d170a
Merge branch 'main' into fix/gtm-telemetry-entrypoint
simula-r Feb 3, 2026
dfbbac8
fix: track begin checkout in gtm
benceruleanlu Feb 4, 2026
6fa89b8
chore: move dist telemetry scan into workflow
benceruleanlu Feb 4, 2026
ea49807
feat: add checkout type to begin checkout
benceruleanlu Feb 4, 2026
3b1e425
Merge remote-tracking branch 'origin/main' into fix/gtm-telemetry-ent…
benceruleanlu Feb 4, 2026
6580b76
Fix unit test
benceruleanlu Feb 4, 2026
65b3089
fix: add checkout attribution to gtm
benceruleanlu Feb 5, 2026
56df3eb
fix unit test by hoisting
benceruleanlu Feb 5, 2026
4d9b015
Switch to __DISTRIBUTION__ in main.ts
benceruleanlu Feb 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface Window {
badge?: string
}
}
dataLayer?: Array<Record<string, unknown>>
Copy link
Contributor

Choose a reason for hiding this comment

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

In src/platform/telemetry/providers/cloud/GtmTelemetryProvider.ts, this is declared as dataLayer?: Record<string, unknown>[] which is distinct from Array<Record<string, unknown>>. Is the goal to assert non-nullability if the gtm module is loaded?

Copy link
Member Author

Choose a reason for hiding this comment

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

We’re not trying to assert non‑nullability, I removed the duplicate declare block in GtmTelemetryProvider.ts and kept the single optional type in global.d.ts. The provider initializes window.dataLayer when a GTM ID exists and still guards with ?.push for safety.

}

interface Navigator {
Expand Down
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ if (isCloud) {
const { refreshRemoteConfig } =
await import('@/platform/remoteConfig/refreshRemoteConfig')
await refreshRemoteConfig({ useAuth: false })

const { initGtm } = await import('@/platform/telemetry')
initGtm()
}

const ComfyUIPreset = definePreset(Aura, {
Expand Down
145 changes: 113 additions & 32 deletions src/platform/cloud/subscription/composables/useSubscription.test.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,54 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { effectScope } from 'vue'

import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'

// Create mocks
const mockIsLoggedIn = ref(false)
const mockReportError = vi.fn()
const mockAccessBillingPortal = vi.fn()
const mockShowSubscriptionRequiredDialog = vi.fn()
const mockGetAuthHeader = vi.fn(() =>
Promise.resolve({ Authorization: 'Bearer test-token' })
)
const mockTelemetry = {
trackSubscription: vi.fn(),
trackMonthlySubscriptionCancelled: vi.fn()
const {
mockIsLoggedIn,
mockReportError,
mockAccessBillingPortal,
mockShowSubscriptionRequiredDialog,
mockGetAuthHeader,
mockPushDataLayerEvent,
mockTelemetry
} = vi.hoisted(() => ({
mockIsLoggedIn: { value: false },
mockReportError: vi.fn(),
mockAccessBillingPortal: vi.fn(),
mockShowSubscriptionRequiredDialog: vi.fn(),
mockGetAuthHeader: vi.fn(() =>
Promise.resolve({ Authorization: 'Bearer test-token' })
),
mockPushDataLayerEvent: vi.fn(),
mockTelemetry: {
trackSubscription: vi.fn(),
trackMonthlySubscriptionCancelled: vi.fn()
}
}))

let scope: ReturnType<typeof effectScope> | undefined

function useSubscriptionWithScope() {
Copy link
Contributor

Choose a reason for hiding this comment

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

this looks like it belongs in the main file and not the test file

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, it's intentionally here as test-only scaffolding, It's a small helper

if (!scope) {
throw new Error('Test scope not initialized')
}

const subscription = scope.run(() => useSubscription())
if (!subscription) {
throw new Error('Failed to initialize subscription composable')
}

return subscription
Comment on lines +33 to +43
Copy link
Contributor

Choose a reason for hiding this comment

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

question: How did you come up with this approach? Looks interesting, never would have thought of this. cc: @DrJKL

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't remember, but it was required for the test to run properly.

}

// Mock dependencies
vi.mock('@/composables/auth/useCurrentUser', () => ({
useCurrentUser: vi.fn(() => ({
isLoggedIn: mockIsLoggedIn
}))
}))

vi.mock('@/platform/telemetry', () => ({
pushDataLayerEvent: mockPushDataLayerEvent,
useTelemetry: vi.fn(() => mockTelemetry)
}))

Expand Down Expand Up @@ -73,11 +98,24 @@ vi.mock('@/stores/firebaseAuthStore', () => ({
global.fetch = vi.fn()

describe('useSubscription', () => {
afterEach(() => {
scope?.stop()
scope = undefined
})

beforeEach(() => {
scope?.stop()
scope = effectScope()

vi.clearAllMocks()
mockIsLoggedIn.value = false
mockTelemetry.trackSubscription.mockReset()
mockTelemetry.trackMonthlySubscriptionCancelled.mockReset()
mockPushDataLayerEvent.mockReset()
mockPushDataLayerEvent.mockImplementation((event) => {
const dataLayer = window.dataLayer ?? (window.dataLayer = [])
dataLayer.push(event)
})
window.__CONFIG__ = {
subscription_required: true
} as typeof window.__CONFIG__
Expand All @@ -103,7 +141,7 @@ describe('useSubscription', () => {
} as Response)

mockIsLoggedIn.value = true
const { isActiveSubscription, fetchStatus } = useSubscription()
const { isActiveSubscription, fetchStatus } = useSubscriptionWithScope()

await fetchStatus()
expect(isActiveSubscription.value).toBe(true)
Expand All @@ -120,7 +158,7 @@ describe('useSubscription', () => {
} as Response)

mockIsLoggedIn.value = true
const { isActiveSubscription, fetchStatus } = useSubscription()
const { isActiveSubscription, fetchStatus } = useSubscriptionWithScope()

await fetchStatus()
expect(isActiveSubscription.value).toBe(false)
Expand All @@ -137,7 +175,7 @@ describe('useSubscription', () => {
} as Response)

mockIsLoggedIn.value = true
const { formattedRenewalDate, fetchStatus } = useSubscription()
const { formattedRenewalDate, fetchStatus } = useSubscriptionWithScope()

await fetchStatus()
// The date format may vary based on timezone, so we just check it's a valid date string
Expand All @@ -147,7 +185,7 @@ describe('useSubscription', () => {
})

it('should return empty string when renewal date is not available', () => {
const { formattedRenewalDate } = useSubscription()
const { formattedRenewalDate } = useSubscriptionWithScope()

expect(formattedRenewalDate.value).toBe('')
})
Expand All @@ -164,14 +202,14 @@ describe('useSubscription', () => {
} as Response)

mockIsLoggedIn.value = true
const { subscriptionTier, fetchStatus } = useSubscription()
const { subscriptionTier, fetchStatus } = useSubscriptionWithScope()

await fetchStatus()
expect(subscriptionTier.value).toBe('CREATOR')
})

it('should return null when subscription tier is not available', () => {
const { subscriptionTier } = useSubscription()
const { subscriptionTier } = useSubscriptionWithScope()

expect(subscriptionTier.value).toBeNull()
})
Expand All @@ -191,7 +229,7 @@ describe('useSubscription', () => {
} as Response)

mockIsLoggedIn.value = true
const { fetchStatus } = useSubscription()
const { fetchStatus } = useSubscriptionWithScope()

await fetchStatus()

Expand All @@ -206,13 +244,56 @@ describe('useSubscription', () => {
)
})

it('pushes purchase event after a pending subscription completes', async () => {
window.dataLayer = []
localStorage.setItem(
'pending_subscription_purchase',
JSON.stringify({
tierKey: 'creator',
billingCycle: 'monthly',
timestamp: Date.now()
})
)

vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: async () => ({
is_active: true,
subscription_id: 'sub_123',
subscription_tier: 'CREATOR',
subscription_duration: 'MONTHLY'
})
} as Response)

mockIsLoggedIn.value = true
const { fetchStatus } = useSubscriptionWithScope()

await fetchStatus()

expect(window.dataLayer).toHaveLength(1)
expect(window.dataLayer?.[0]).toMatchObject({
event: 'purchase',
transaction_id: 'sub_123',
currency: 'USD',
items: [
{
item_id: 'monthly_creator',
item_variant: 'monthly',
item_category: 'subscription',
quantity: 1
}
]
})
expect(localStorage.getItem('pending_subscription_purchase')).toBeNull()
})

it('should handle fetch errors gracefully', async () => {
vi.mocked(global.fetch).mockResolvedValue({
ok: false,
json: async () => ({ message: 'Subscription not found' })
} as Response)

const { fetchStatus } = useSubscription()
const { fetchStatus } = useSubscriptionWithScope()

await expect(fetchStatus()).rejects.toThrow()
})
Expand All @@ -232,7 +313,7 @@ describe('useSubscription', () => {
.spyOn(window, 'open')
.mockImplementation(() => null)

const { subscribe } = useSubscription()
const { subscribe } = useSubscriptionWithScope()

await subscribe()

Expand All @@ -258,7 +339,7 @@ describe('useSubscription', () => {
json: async () => ({})
} as Response)

const { subscribe } = useSubscription()
const { subscribe } = useSubscriptionWithScope()

await expect(subscribe()).rejects.toThrow()
})
Expand All @@ -275,7 +356,7 @@ describe('useSubscription', () => {
})
} as Response)

const { requireActiveSubscription } = useSubscription()
const { requireActiveSubscription } = useSubscriptionWithScope()

await requireActiveSubscription()

Expand All @@ -292,7 +373,7 @@ describe('useSubscription', () => {
})
} as Response)

const { requireActiveSubscription } = useSubscription()
const { requireActiveSubscription } = useSubscriptionWithScope()

await requireActiveSubscription()

Expand All @@ -306,7 +387,7 @@ describe('useSubscription', () => {
.spyOn(window, 'open')
.mockImplementation(() => null)

const { handleViewUsageHistory } = useSubscription()
const { handleViewUsageHistory } = useSubscriptionWithScope()
handleViewUsageHistory()

expect(windowOpenSpy).toHaveBeenCalledWith(
Expand All @@ -322,7 +403,7 @@ describe('useSubscription', () => {
.spyOn(window, 'open')
.mockImplementation(() => null)

const { handleLearnMore } = useSubscription()
const { handleLearnMore } = useSubscriptionWithScope()
handleLearnMore()

expect(windowOpenSpy).toHaveBeenCalledWith(
Expand All @@ -334,15 +415,15 @@ describe('useSubscription', () => {
})

it('should call accessBillingPortal for invoice history', async () => {
const { handleInvoiceHistory } = useSubscription()
const { handleInvoiceHistory } = useSubscriptionWithScope()

await handleInvoiceHistory()

expect(mockAccessBillingPortal).toHaveBeenCalled()
})

it('should call accessBillingPortal for manage subscription', async () => {
const { manageSubscription } = useSubscription()
const { manageSubscription } = useSubscriptionWithScope()

await manageSubscription()

Expand Down Expand Up @@ -378,7 +459,7 @@ describe('useSubscription', () => {
.mockResolvedValueOnce(cancelledResponse as Response)

try {
const { fetchStatus, manageSubscription } = useSubscription()
const { fetchStatus, manageSubscription } = useSubscriptionWithScope()

await fetchStatus()
await manageSubscription()
Expand Down Expand Up @@ -422,7 +503,7 @@ describe('useSubscription', () => {
.mockResolvedValueOnce(cancelledResponse as Response)

try {
const { fetchStatus, manageSubscription } = useSubscription()
const { fetchStatus, manageSubscription } = useSubscriptionWithScope()

await fetchStatus()
await manageSubscription()
Expand Down
Loading
Loading