Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 52 additions & 0 deletions .github/workflows/ci-dist-telemetry-scan.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: 'CI: Dist Telemetry Scan'

on:
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
scan:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

Check warning on line 19 in .github/workflows/ci-dist-telemetry-scan.yaml

View workflow job for this annotation

GitHub Actions / yaml-lint

19:73 [comments] too few spaces before comment: expected 2

- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0

Check warning on line 22 in .github/workflows/ci-dist-telemetry-scan.yaml

View workflow job for this annotation

GitHub Actions / yaml-lint

22:74 [comments] too few spaces before comment: expected 2
with:
version: 10

- name: Use Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0

Check warning on line 27 in .github/workflows/ci-dist-telemetry-scan.yaml

View workflow job for this annotation

GitHub Actions / yaml-lint

27:75 [comments] too few spaces before comment: expected 2
with:
node-version: 'lts/*'
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build project
run: pnpm build

- name: Scan dist for telemetry references
run: |
set -euo pipefail
if rg --no-ignore -n \
-g '*.html' \
-g '*.js' \
-e 'Google Tag Manager' \
-e '(?i)\bgtm\.js\b' \
-e '(?i)googletagmanager\.com/gtm\.js\\?id=' \
-e '(?i)googletagmanager\.com/ns\.html\\?id=' \
Comment on lines +46 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fix GTM URL regex escaping in dist telemetry scan

The new scan patterns use \?id= (googletagmanager\.com/...\\?id=), which in ripgrep regex does not match a literal ?id= URL query; as a result, GTM URL references like .../ns.html?id=... can slip past CI undetected. This weakens the guardrail the workflow is intended to enforce for telemetry-free dist assets.

Useful? React with 👍 / 👎.

dist; then
echo 'Telemetry references found in dist assets.'
exit 1
fi
echo 'No telemetry references found in dist assets.'
7 changes: 7 additions & 0 deletions global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ declare const __USE_PROD_CONFIG__: boolean

interface Window {
__CONFIG__: {
gtm_container_id?: string
mixpanel_token?: string
require_whitelist?: boolean
subscription_required?: boolean
Expand All @@ -30,6 +31,12 @@ interface Window {
badge?: string
}
}
__ga_identity__?: {
client_id?: string
session_id?: string
session_number?: string
}
dataLayer?: Array<Record<string, unknown>>
}

interface Navigator {
Expand Down
5 changes: 4 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@ import { i18n } from './i18n'
* CRITICAL: Load remote config FIRST for cloud builds to ensure
* window.__CONFIG__is available for all modules during initialization
*/
import { isCloud } from '@/platform/distribution/types'
const isCloud = __DISTRIBUTION__ === 'cloud'

if (isCloud) {
const { refreshRemoteConfig } =
await import('@/platform/remoteConfig/refreshRemoteConfig')
await refreshRemoteConfig({ useAuth: false })

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

const ComfyUIPreset = definePreset(Aura, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ vi.mock('@/composables/useErrorHandling', () => ({

const subscriptionMocks = vi.hoisted(() => ({
isActiveSubscription: { value: false },
isInitialized: { value: true }
isInitialized: { value: true },
subscriptionStatus: { value: null }
}))

vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
Expand Down
26 changes: 24 additions & 2 deletions src/platform/cloud/subscription/components/PricingTable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@ const mockSubscriptionTier = ref<
const mockIsYearlySubscription = ref(false)
const mockAccessBillingPortal = vi.fn()
const mockReportError = vi.fn()
const mockTrackBeginCheckout = vi.fn()
const mockGetFirebaseAuthHeader = vi.fn(() =>
Promise.resolve({ Authorization: 'Bearer test-token' })
)
const mockGetCheckoutAttribution = vi.hoisted(() => vi.fn(() => ({})))

vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
useSubscription: () => ({
isActiveSubscription: computed(() => mockIsActiveSubscription.value),
subscriptionTier: computed(() => mockSubscriptionTier.value),
isYearlySubscription: computed(() => mockIsYearlySubscription.value)
isYearlySubscription: computed(() => mockIsYearlySubscription.value),
subscriptionStatus: ref(null)
})
}))

Expand Down Expand Up @@ -53,11 +56,22 @@ vi.mock('@/composables/useErrorHandling', () => ({

vi.mock('@/stores/firebaseAuthStore', () => ({
useFirebaseAuthStore: () => ({
getFirebaseAuthHeader: mockGetFirebaseAuthHeader
getFirebaseAuthHeader: mockGetFirebaseAuthHeader,
userId: 'user-123'

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

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

This test mocks userId as a plain string, but the real useFirebaseAuthStore().userId is a computed ref. That mismatch makes it easy for ref/value bugs to slip through; mock userId as a ref/computed and assert the implementation reads userId.value.

Suggested change
userId: 'user-123'
userId: computed(() => 'user-123')

Copilot uses AI. Check for mistakes.
}),
FirebaseAuthStoreError: class extends Error {}
}))

vi.mock('@/platform/telemetry', () => ({
useTelemetry: () => ({
trackBeginCheckout: mockTrackBeginCheckout
})
}))

vi.mock('@/platform/telemetry/utils/checkoutAttribution', () => ({
getCheckoutAttribution: mockGetCheckoutAttribution
}))

vi.mock('@/platform/distribution/types', () => ({
isCloud: true
}))
Expand Down Expand Up @@ -126,6 +140,7 @@ describe('PricingTable', () => {
mockIsActiveSubscription.value = false
mockSubscriptionTier.value = null
mockIsYearlySubscription.value = false
mockTrackBeginCheckout.mockReset()
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: async () => ({ checkout_url: 'https://checkout.stripe.com/test' })
Expand All @@ -148,6 +163,13 @@ describe('PricingTable', () => {
await creatorButton?.trigger('click')
await flushPromises()

expect(mockTrackBeginCheckout).toHaveBeenCalledWith({
user_id: 'user-123',
tier: 'creator',
cycle: 'yearly',
checkout_type: 'change',
previous_tier: 'standard'
})
expect(mockAccessBillingPortal).toHaveBeenCalledWith('creator-yearly')
})

Expand Down
24 changes: 23 additions & 1 deletion src/platform/cloud/subscription/components/PricingTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ import { performSubscriptionCheckout } from '@/platform/cloud/subscription/utils
import { isPlanDowngrade } from '@/platform/cloud/subscription/utils/subscriptionTierRank'
import type { BillingCycle } from '@/platform/cloud/subscription/utils/subscriptionTierRank'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { getCheckoutAttribution } from '@/platform/telemetry/utils/checkoutAttribution'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import type { components } from '@/types/comfyRegistryTypes'

type SubscriptionTier = components['schemas']['SubscriptionTier']
Expand Down Expand Up @@ -330,6 +333,8 @@ const tiers: PricingTierConfig[] = [
const { n } = useI18n()
const { isActiveSubscription, subscriptionTier, isYearlySubscription } =
useSubscription()
const telemetry = useTelemetry()
const { userId } = useFirebaseAuthStore()
const { accessBillingPortal, reportError } = useFirebaseAuthActions()
const { wrapWithErrorHandlingAsync } = useErrorHandling()

Expand Down Expand Up @@ -410,6 +415,19 @@ const handleSubscribe = wrapWithErrorHandlingAsync(

try {
if (isActiveSubscription.value) {
const checkoutAttribution = getCheckoutAttribution()
if (userId) {
telemetry?.trackBeginCheckout({
user_id: userId,
Comment on lines +419 to +421

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

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

userId from useFirebaseAuthStore() is a computed ref in the actual Pinia setup store. As written, if (userId) will always pass and user_id: userId will push a ref object to telemetry. Use userId.value (or storeToRefs(useFirebaseAuthStore())) when checking and when populating user_id.

Suggested change
if (userId) {
telemetry?.trackBeginCheckout({
user_id: userId,
if (userId?.value) {
telemetry?.trackBeginCheckout({
user_id: userId.value,

Copilot uses AI. Check for mistakes.
tier: tierKey,
cycle: currentBillingCycle.value,
checkout_type: 'change',
...checkoutAttribution,
...(currentTierKey.value
? { previous_tier: currentTierKey.value }
: {})
})
}
// Pass the target tier to create a deep link to subscription update confirmation
const checkoutTier = getCheckoutTier(tierKey, currentBillingCycle.value)
const targetPlan = {
Expand All @@ -430,7 +448,11 @@ const handleSubscribe = wrapWithErrorHandlingAsync(
await accessBillingPortal(checkoutTier)
}
} else {
await performSubscriptionCheckout(tierKey, currentBillingCycle.value)
await performSubscriptionCheckout(
tierKey,
currentBillingCycle.value,
true
)
}
} finally {
isLoading.value = false
Expand Down
Loading
Loading