mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-05 20:54:56 +00:00
Compare commits
1 Commits
main
...
nav/fe-945
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2177b67d2f |
20
apps/website/src/env.d.ts
vendored
20
apps/website/src/env.d.ts
vendored
@@ -1 +1,21 @@
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly PUBLIC_SYFT_SOURCE_ID?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
interface Window {
|
||||
syft?: {
|
||||
identify: (...args: unknown[]) => void
|
||||
signup: (...args: unknown[]) => void
|
||||
track: (...args: unknown[]) => void
|
||||
page: (...args: unknown[]) => void
|
||||
q?: unknown[][]
|
||||
fi?: unknown[]
|
||||
}
|
||||
syftc?: { sourceId: string }
|
||||
}
|
||||
|
||||
@@ -149,11 +149,13 @@ const websiteJsonLd = {
|
||||
import { initSmoothScroll, cancelScroll } from '../scripts/smoothScroll'
|
||||
import { ScrollTrigger } from '../scripts/gsapSetup'
|
||||
import { initPostHog, capturePageview } from '../scripts/posthog'
|
||||
import { initSyft } from '../scripts/syft'
|
||||
|
||||
initSmoothScroll()
|
||||
|
||||
if (import.meta.env.PROD) {
|
||||
initPostHog()
|
||||
initSyft()
|
||||
document.addEventListener('astro:page-load', capturePageview)
|
||||
}
|
||||
|
||||
|
||||
41
apps/website/src/scripts/syft.ts
Normal file
41
apps/website/src/scripts/syft.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
const SYFT_SOURCE_ID =
|
||||
import.meta.env.PUBLIC_SYFT_SOURCE_ID ?? 'cmo1xgq4o000804jr5jlj5dtn'
|
||||
const SYFT_SRC = 'https://cdn.sy-d.io/syftnext/syft.umd.js'
|
||||
|
||||
let initialized = false
|
||||
|
||||
/**
|
||||
* Loads the SyftData snippet for anonymous visitor observation.
|
||||
* Idempotent with the Syft tag in GTM: no-ops if `window.syft` exists.
|
||||
*/
|
||||
export function initSyft() {
|
||||
if (initialized || typeof window === 'undefined' || !SYFT_SOURCE_ID) return
|
||||
try {
|
||||
window.syftc = { sourceId: SYFT_SOURCE_ID }
|
||||
if (window.syft) {
|
||||
initialized = true
|
||||
return
|
||||
}
|
||||
const q: unknown[][] = []
|
||||
const enqueue =
|
||||
(method: string) =>
|
||||
(...args: unknown[]) => {
|
||||
q.push([method, ...args])
|
||||
}
|
||||
window.syft = {
|
||||
q,
|
||||
fi: [],
|
||||
identify: enqueue('identify'),
|
||||
signup: enqueue('signup'),
|
||||
track: enqueue('track'),
|
||||
page: enqueue('page')
|
||||
}
|
||||
const script = document.createElement('script')
|
||||
script.src = SYFT_SRC
|
||||
script.async = true
|
||||
document.head.appendChild(script)
|
||||
initialized = true
|
||||
} catch (error) {
|
||||
console.error('Syft init failed', error)
|
||||
}
|
||||
}
|
||||
1
global.d.ts
vendored
1
global.d.ts
vendored
@@ -49,6 +49,7 @@ interface Window {
|
||||
posthog_project_token?: string
|
||||
posthog_api_host?: string
|
||||
posthog_config?: Record<string, unknown>
|
||||
syftdata_source_id?: string
|
||||
require_whitelist?: boolean
|
||||
subscription_required?: boolean
|
||||
max_upload_size?: number
|
||||
|
||||
@@ -82,6 +82,7 @@ export type RemoteConfig = {
|
||||
posthog_project_token?: string
|
||||
posthog_api_host?: string
|
||||
posthog_config?: Partial<PostHogConfig>
|
||||
syftdata_source_id?: string
|
||||
subscription_required?: boolean
|
||||
server_health_alert?: ServerHealthAlert
|
||||
max_upload_size?: number
|
||||
|
||||
@@ -26,14 +26,16 @@ export async function initTelemetry(): Promise<void> {
|
||||
{ GtmTelemetryProvider },
|
||||
{ ImpactTelemetryProvider },
|
||||
{ PostHogTelemetryProvider },
|
||||
{ ClickHouseTelemetryProvider }
|
||||
{ ClickHouseTelemetryProvider },
|
||||
{ SyftTelemetryProvider }
|
||||
] = await Promise.all([
|
||||
import('./TelemetryRegistry'),
|
||||
import('./providers/cloud/MixpanelTelemetryProvider'),
|
||||
import('./providers/cloud/GtmTelemetryProvider'),
|
||||
import('./providers/cloud/ImpactTelemetryProvider'),
|
||||
import('./providers/cloud/PostHogTelemetryProvider'),
|
||||
import('./providers/cloud/ClickHouseTelemetryProvider')
|
||||
import('./providers/cloud/ClickHouseTelemetryProvider'),
|
||||
import('./providers/cloud/SyftTelemetryProvider')
|
||||
])
|
||||
|
||||
const registry = new TelemetryRegistry()
|
||||
@@ -42,6 +44,7 @@ export async function initTelemetry(): Promise<void> {
|
||||
registry.registerProvider(new ImpactTelemetryProvider())
|
||||
registry.registerProvider(new PostHogTelemetryProvider())
|
||||
registry.registerProvider(new ClickHouseTelemetryProvider())
|
||||
registry.registerProvider(new SyftTelemetryProvider())
|
||||
|
||||
setTelemetryRegistry(registry)
|
||||
})()
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mockUserState = vi.hoisted(() => ({
|
||||
onUserResolved: vi.fn(),
|
||||
userEmail: { value: undefined as string | undefined }
|
||||
}))
|
||||
vi.mock('@/composables/auth/useCurrentUser', () => ({
|
||||
useCurrentUser: () => mockUserState
|
||||
}))
|
||||
|
||||
const SYFT_SRC = 'https://cdn.sy-d.io/syftnext/syft.umd.js'
|
||||
|
||||
type ConfigWindow = { __CONFIG__?: { syftdata_source_id?: string } }
|
||||
|
||||
const importProvider = async () => {
|
||||
const { SyftTelemetryProvider } =
|
||||
await import('@/platform/telemetry/providers/cloud/SyftTelemetryProvider')
|
||||
return SyftTelemetryProvider
|
||||
}
|
||||
|
||||
const querySyftScripts = () =>
|
||||
document.querySelectorAll(`script[src="${SYFT_SRC}"]`)
|
||||
|
||||
describe('SyftTelemetryProvider', () => {
|
||||
let warn: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
querySyftScripts().forEach((el) => el.remove())
|
||||
delete window.syft
|
||||
delete window.syftc
|
||||
delete (window as ConfigWindow).__CONFIG__
|
||||
mockUserState.userEmail.value = undefined
|
||||
warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
warn.mockRestore()
|
||||
})
|
||||
|
||||
describe('initialization', () => {
|
||||
it('warns and skips snippet load when no source id is configured', async () => {
|
||||
const SyftTelemetryProvider = await importProvider()
|
||||
new SyftTelemetryProvider()
|
||||
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Syft source id')
|
||||
)
|
||||
expect(window.syft).toBeUndefined()
|
||||
expect(querySyftScripts()).toHaveLength(0)
|
||||
expect(mockUserState.onUserResolved).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('installs the queue stub and injects the snippet script', async () => {
|
||||
;(window as ConfigWindow).__CONFIG__ = { syftdata_source_id: 'src-123' }
|
||||
const appendChild = vi.spyOn(document.head, 'appendChild')
|
||||
|
||||
try {
|
||||
const SyftTelemetryProvider = await importProvider()
|
||||
new SyftTelemetryProvider()
|
||||
|
||||
expect(window.syftc).toEqual({ sourceId: 'src-123' })
|
||||
expect(window.syft?.q).toEqual([])
|
||||
const appended = appendChild.mock.calls[0]?.[0]
|
||||
expect(appended).toBeInstanceOf(HTMLScriptElement)
|
||||
expect((appended as HTMLScriptElement).src).toBe(SYFT_SRC)
|
||||
} finally {
|
||||
appendChild.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('queues identify calls made before the script loads', async () => {
|
||||
;(window as ConfigWindow).__CONFIG__ = { syftdata_source_id: 'src-123' }
|
||||
|
||||
const SyftTelemetryProvider = await importProvider()
|
||||
new SyftTelemetryProvider()
|
||||
window.syft?.identify('user@example.com', { source: 'login' })
|
||||
|
||||
expect(window.syft?.q).toContainEqual([
|
||||
'identify',
|
||||
'user@example.com',
|
||||
{ source: 'login' }
|
||||
])
|
||||
})
|
||||
|
||||
it('preserves an existing window.syft and script (GTM-loaded)', async () => {
|
||||
;(window as ConfigWindow).__CONFIG__ = { syftdata_source_id: 'src-123' }
|
||||
const gtmSyft = {
|
||||
identify: vi.fn(),
|
||||
signup: vi.fn(),
|
||||
track: vi.fn(),
|
||||
page: vi.fn()
|
||||
}
|
||||
window.syft = gtmSyft
|
||||
const gtmScript = document.createElement('script')
|
||||
gtmScript.src = SYFT_SRC
|
||||
document.head.appendChild(gtmScript)
|
||||
|
||||
const SyftTelemetryProvider = await importProvider()
|
||||
new SyftTelemetryProvider()
|
||||
|
||||
expect(window.syft).toBe(gtmSyft)
|
||||
expect(querySyftScripts()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('identifies with email and login source on user resolve', async () => {
|
||||
;(window as ConfigWindow).__CONFIG__ = { syftdata_source_id: 'src-123' }
|
||||
mockUserState.userEmail.value = 'user@example.com'
|
||||
|
||||
const SyftTelemetryProvider = await importProvider()
|
||||
new SyftTelemetryProvider()
|
||||
const resolveCallback = mockUserState.onUserResolved.mock.calls[0][0]
|
||||
resolveCallback()
|
||||
|
||||
expect(window.syft?.q).toContainEqual([
|
||||
'identify',
|
||||
'user@example.com',
|
||||
{ source: 'login' }
|
||||
])
|
||||
})
|
||||
|
||||
it('skips identify on user resolve when email is unavailable', async () => {
|
||||
;(window as ConfigWindow).__CONFIG__ = { syftdata_source_id: 'src-123' }
|
||||
|
||||
const SyftTelemetryProvider = await importProvider()
|
||||
new SyftTelemetryProvider()
|
||||
const resolveCallback = mockUserState.onUserResolved.mock.calls[0][0]
|
||||
resolveCallback()
|
||||
|
||||
expect(window.syft?.q).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('trackAuth', () => {
|
||||
const installSyftSpy = () => {
|
||||
const syftSpy = {
|
||||
identify: vi.fn(),
|
||||
signup: vi.fn(),
|
||||
track: vi.fn(),
|
||||
page: vi.fn()
|
||||
}
|
||||
window.syft = syftSpy
|
||||
return syftSpy
|
||||
}
|
||||
|
||||
it('calls signup for new users with method and signup source', async () => {
|
||||
const syftSpy = installSyftSpy()
|
||||
|
||||
const SyftTelemetryProvider = await importProvider()
|
||||
new SyftTelemetryProvider().trackAuth({
|
||||
method: 'google',
|
||||
is_new_user: true,
|
||||
email: 'new@example.com'
|
||||
})
|
||||
|
||||
expect(syftSpy.signup).toHaveBeenCalledWith('new@example.com', {
|
||||
source: 'signup',
|
||||
method: 'google'
|
||||
})
|
||||
expect(syftSpy.identify).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('calls identify for returning users with login source', async () => {
|
||||
const syftSpy = installSyftSpy()
|
||||
|
||||
const SyftTelemetryProvider = await importProvider()
|
||||
new SyftTelemetryProvider().trackAuth({
|
||||
method: 'github',
|
||||
is_new_user: false,
|
||||
email: 'back@example.com'
|
||||
})
|
||||
|
||||
expect(syftSpy.identify).toHaveBeenCalledWith('back@example.com', {
|
||||
source: 'login',
|
||||
method: 'github'
|
||||
})
|
||||
expect(syftSpy.signup).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('defaults to login source when new vs returning is unknown', async () => {
|
||||
const syftSpy = installSyftSpy()
|
||||
|
||||
const SyftTelemetryProvider = await importProvider()
|
||||
new SyftTelemetryProvider().trackAuth({
|
||||
method: 'email',
|
||||
email: 'who@example.com'
|
||||
})
|
||||
|
||||
expect(syftSpy.identify).toHaveBeenCalledWith('who@example.com', {
|
||||
source: 'login',
|
||||
method: 'email'
|
||||
})
|
||||
expect(syftSpy.signup).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does nothing without an email', async () => {
|
||||
const syftSpy = installSyftSpy()
|
||||
|
||||
const SyftTelemetryProvider = await importProvider()
|
||||
new SyftTelemetryProvider().trackAuth({
|
||||
method: 'email',
|
||||
is_new_user: true
|
||||
})
|
||||
|
||||
expect(syftSpy.signup).not.toHaveBeenCalled()
|
||||
expect(syftSpy.identify).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
143
src/platform/telemetry/providers/cloud/SyftTelemetryProvider.ts
Normal file
143
src/platform/telemetry/providers/cloud/SyftTelemetryProvider.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* SyftData Telemetry Provider - Cloud Build Implementation
|
||||
*
|
||||
* Loads the Syft snippet and identifies logged-in users by email so Syft
|
||||
* can enrich them (firmographics flow to PostHog via Syft's native
|
||||
* destination as `sy_*` person properties). Only implements trackAuth -
|
||||
* page/session capture is handled by Syft's own autotrack.
|
||||
*
|
||||
* The loader is idempotent with the Syft tag in GTM: whichever runs
|
||||
* first wins.
|
||||
*
|
||||
* CRITICAL: OSS Build Safety
|
||||
* Entire file is tree-shaken away in OSS builds (DISTRIBUTION unset).
|
||||
*/
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
|
||||
import type { AuthMetadata, TelemetryProvider } from '../../types'
|
||||
|
||||
const SYFT_SRC = 'https://cdn.sy-d.io/syftnext/syft.umd.js'
|
||||
|
||||
type SyftTraits = Record<string, string | number | null | undefined>
|
||||
|
||||
interface SyftPendingFetch {
|
||||
args: unknown[]
|
||||
resolve: (value: unknown) => void
|
||||
reject: (reason?: unknown) => void
|
||||
}
|
||||
|
||||
interface SyftClient {
|
||||
identify: (email: string, traits?: SyftTraits) => void
|
||||
signup: (email: string, traits?: SyftTraits) => void
|
||||
track: (event: string, traits?: SyftTraits) => void
|
||||
page: (...args: unknown[]) => void
|
||||
q?: unknown[][]
|
||||
fi?: SyftPendingFetch[]
|
||||
fetchID?: (...args: unknown[]) => Promise<unknown>
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
syft?: SyftClient
|
||||
syftc?: { sourceId: string }
|
||||
}
|
||||
}
|
||||
|
||||
/** Pre-load queue stub matching the official Syft loader snippet; the UMD
|
||||
* script drains `q` and settles `fi` entries once it loads. */
|
||||
function createSyftStub(): SyftClient {
|
||||
const q: unknown[][] = []
|
||||
const fi: SyftPendingFetch[] = []
|
||||
const enqueue =
|
||||
(method: string) =>
|
||||
(...args: unknown[]) => {
|
||||
q.push([method, ...args])
|
||||
}
|
||||
return {
|
||||
q,
|
||||
fi,
|
||||
fetchID: (...args: unknown[]) =>
|
||||
new Promise((resolve, reject) => {
|
||||
fi.push({ args, resolve, reject })
|
||||
}),
|
||||
identify: enqueue('identify'),
|
||||
signup: enqueue('signup'),
|
||||
track: enqueue('track'),
|
||||
page: enqueue('page')
|
||||
}
|
||||
}
|
||||
|
||||
let scriptPromise: Promise<void> | null = null
|
||||
|
||||
function ensureSyftLoaded(sourceId: string): Promise<void> {
|
||||
if (scriptPromise) return scriptPromise
|
||||
|
||||
scriptPromise = new Promise<void>((resolve, reject) => {
|
||||
window.syftc = { sourceId }
|
||||
if (!window.syft) {
|
||||
window.syft = createSyftStub()
|
||||
}
|
||||
|
||||
const existing = document.querySelector<HTMLScriptElement>(
|
||||
`script[src="${SYFT_SRC}"]`
|
||||
)
|
||||
if (existing) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const scriptEl = document.createElement('script')
|
||||
scriptEl.src = SYFT_SRC
|
||||
scriptEl.async = true
|
||||
scriptEl.addEventListener('load', () => resolve(), { once: true })
|
||||
scriptEl.addEventListener(
|
||||
'error',
|
||||
() => {
|
||||
scriptEl.remove()
|
||||
scriptPromise = null
|
||||
reject(new Error('Syft script failed to load'))
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
document.head.appendChild(scriptEl)
|
||||
})
|
||||
|
||||
return scriptPromise
|
||||
}
|
||||
|
||||
export class SyftTelemetryProvider implements TelemetryProvider {
|
||||
constructor() {
|
||||
const sourceId = window.__CONFIG__?.syftdata_source_id
|
||||
if (sourceId) {
|
||||
ensureSyftLoaded(sourceId).catch((error) => {
|
||||
console.warn('[Syft] snippet failed to load', error)
|
||||
})
|
||||
} else {
|
||||
console.warn('Syft source id not provided in runtime config')
|
||||
}
|
||||
|
||||
// Re-identify on every session resolve so users who were already
|
||||
// logged in before this shipped still get linked to their Syft
|
||||
// profile. Session restores cannot distinguish new vs returning,
|
||||
// so source is 'login'. Registered even without a sourceId because
|
||||
// the GTM-loaded snippet may still provide window.syft.
|
||||
const currentUser = useCurrentUser()
|
||||
currentUser.onUserResolved(() => {
|
||||
const email = currentUser.userEmail.value
|
||||
if (email) {
|
||||
window.syft?.identify(email, { source: 'login' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Identify at the auth action itself - the only moment signup vs
|
||||
* login ("source") and auth method are knowable. */
|
||||
trackAuth({ email, is_new_user, method }: AuthMetadata): void {
|
||||
if (!email) return
|
||||
if (is_new_user) {
|
||||
window.syft?.signup(email, { source: 'signup', method })
|
||||
} else {
|
||||
window.syft?.identify(email, { source: 'login', method })
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user