-
Notifications
You must be signed in to change notification settings - Fork 511
fix: route gtm through telemetry entrypoint #8354
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
ede4aa2
c247d7b
563cdbf
2947a99
2db667b
074ec62
bca2c62
a3ccd92
c0ee820
acbaf04
c375b53
f2cf5ab
a43f596
d864e7e
969134e
66cee7d
c439ab1
a4fce64
dd2e18d
921c168
0d2dc2f
8c75f14
894fe54
20a9514
6927c15
22c76c7
3899450
100d0eb
50a2f16
3ec99e8
ea55302
68944ca
233a0c7
294675b
3cd9de4
d898568
2b2043d
320cafd
c29c6fc
c5d170a
dfbbac8
6fa89b8
ea49807
3b1e425
6580b76
65b3089
56df3eb
4d9b015
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| })) | ||
|
|
||
|
|
@@ -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__ | ||
|
|
@@ -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) | ||
|
|
@@ -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) | ||
|
|
@@ -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 | ||
|
|
@@ -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('') | ||
| }) | ||
|
|
@@ -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() | ||
| }) | ||
|
|
@@ -191,7 +229,7 @@ describe('useSubscription', () => { | |
| } as Response) | ||
|
|
||
| mockIsLoggedIn.value = true | ||
| const { fetchStatus } = useSubscription() | ||
| const { fetchStatus } = useSubscriptionWithScope() | ||
|
|
||
| await fetchStatus() | ||
|
|
||
|
|
@@ -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() | ||
| }) | ||
|
|
@@ -232,7 +313,7 @@ describe('useSubscription', () => { | |
| .spyOn(window, 'open') | ||
| .mockImplementation(() => null) | ||
|
|
||
| const { subscribe } = useSubscription() | ||
| const { subscribe } = useSubscriptionWithScope() | ||
|
|
||
| await subscribe() | ||
|
|
||
|
|
@@ -258,7 +339,7 @@ describe('useSubscription', () => { | |
| json: async () => ({}) | ||
| } as Response) | ||
|
|
||
| const { subscribe } = useSubscription() | ||
| const { subscribe } = useSubscriptionWithScope() | ||
|
|
||
| await expect(subscribe()).rejects.toThrow() | ||
| }) | ||
|
|
@@ -275,7 +356,7 @@ describe('useSubscription', () => { | |
| }) | ||
| } as Response) | ||
|
|
||
| const { requireActiveSubscription } = useSubscription() | ||
| const { requireActiveSubscription } = useSubscriptionWithScope() | ||
|
|
||
| await requireActiveSubscription() | ||
|
|
||
|
|
@@ -292,7 +373,7 @@ describe('useSubscription', () => { | |
| }) | ||
| } as Response) | ||
|
|
||
| const { requireActiveSubscription } = useSubscription() | ||
| const { requireActiveSubscription } = useSubscriptionWithScope() | ||
|
|
||
| await requireActiveSubscription() | ||
|
|
||
|
|
@@ -306,7 +387,7 @@ describe('useSubscription', () => { | |
| .spyOn(window, 'open') | ||
| .mockImplementation(() => null) | ||
|
|
||
| const { handleViewUsageHistory } = useSubscription() | ||
| const { handleViewUsageHistory } = useSubscriptionWithScope() | ||
| handleViewUsageHistory() | ||
|
|
||
| expect(windowOpenSpy).toHaveBeenCalledWith( | ||
|
|
@@ -322,7 +403,7 @@ describe('useSubscription', () => { | |
| .spyOn(window, 'open') | ||
| .mockImplementation(() => null) | ||
|
|
||
| const { handleLearnMore } = useSubscription() | ||
| const { handleLearnMore } = useSubscriptionWithScope() | ||
| handleLearnMore() | ||
|
|
||
| expect(windowOpenSpy).toHaveBeenCalledWith( | ||
|
|
@@ -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() | ||
|
|
||
|
|
@@ -378,7 +459,7 @@ describe('useSubscription', () => { | |
| .mockResolvedValueOnce(cancelledResponse as Response) | ||
|
|
||
| try { | ||
| const { fetchStatus, manageSubscription } = useSubscription() | ||
| const { fetchStatus, manageSubscription } = useSubscriptionWithScope() | ||
|
|
||
| await fetchStatus() | ||
| await manageSubscription() | ||
|
|
@@ -422,7 +503,7 @@ describe('useSubscription', () => { | |
| .mockResolvedValueOnce(cancelledResponse as Response) | ||
|
|
||
| try { | ||
| const { fetchStatus, manageSubscription } = useSubscription() | ||
| const { fetchStatus, manageSubscription } = useSubscriptionWithScope() | ||
|
|
||
| await fetchStatus() | ||
| await manageSubscription() | ||
|
|
||
There was a problem hiding this comment.
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 asdataLayer?: Record<string, unknown>[]which is distinct fromArray<Record<string, unknown>>. Is the goal to assert non-nullability if the gtm module is loaded?There was a problem hiding this comment.
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.