mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-18 09:48:09 +00:00
fix: own Cloud RUM initialization in frontend
This commit is contained in:
18
.github/workflows/ci-dist-telemetry-scan.yaml
vendored
18
.github/workflows/ci-dist-telemetry-scan.yaml
vendored
@@ -110,6 +110,24 @@ jobs:
|
||||
fi
|
||||
echo '✅ No PostHog references found'
|
||||
|
||||
- name: Scan dist for Datadog RUM references
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo '🔍 Scanning for Datadog RUM references...'
|
||||
if rg --no-ignore -n \
|
||||
-g '*.html' \
|
||||
-g '*.js' \
|
||||
-e '@datadog/browser-rum' \
|
||||
-e '041a9897-5516-4b1f-a245-1a9aa6895488' \
|
||||
-e 'pub7704486e5b64eb4ff6f62891cda45559' \
|
||||
-e 'comfy-cloud-frontend' \
|
||||
dist; then
|
||||
echo '❌ ERROR: Datadog RUM references found in dist assets!'
|
||||
echo 'Datadog RUM must be properly tree-shaken from OSS builds.'
|
||||
exit 1
|
||||
fi
|
||||
echo '✅ No Datadog RUM references found'
|
||||
|
||||
- name: Scan dist for Customer.io telemetry references
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -162,6 +162,6 @@
|
||||
})()
|
||||
</script>
|
||||
<div id="vue-app"></div>
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
<script type="module" src="src/bootstrap.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"@comfyorg/shared-frontend-utils": "workspace:*",
|
||||
"@comfyorg/tailwind-utils": "workspace:*",
|
||||
"@customerio/cdp-analytics-browser": "catalog:",
|
||||
"@datadog/browser-rum": "catalog:",
|
||||
"@formkit/auto-animate": "catalog:",
|
||||
"@iconify/json": "catalog:",
|
||||
"@primeuix/forms": "catalog:",
|
||||
|
||||
31
pnpm-lock.yaml
generated
31
pnpm-lock.yaml
generated
@@ -27,6 +27,9 @@ catalogs:
|
||||
'@customerio/cdp-analytics-browser':
|
||||
specifier: ^0.5.3
|
||||
version: 0.5.3
|
||||
'@datadog/browser-rum':
|
||||
specifier: ^6.33.0
|
||||
version: 6.33.0
|
||||
'@eslint/js':
|
||||
specifier: ^10.0.1
|
||||
version: 10.0.1
|
||||
@@ -468,6 +471,9 @@ importers:
|
||||
'@customerio/cdp-analytics-browser':
|
||||
specifier: 'catalog:'
|
||||
version: 0.5.3
|
||||
'@datadog/browser-rum':
|
||||
specifier: 'catalog:'
|
||||
version: 6.33.0
|
||||
'@formkit/auto-animate':
|
||||
specifier: 'catalog:'
|
||||
version: 0.9.0
|
||||
@@ -1507,6 +1513,20 @@ packages:
|
||||
'@cyberalien/svg-utils@1.2.15':
|
||||
resolution: {integrity: sha512-ZbKU6npzW5PNocdoLVJYfKzaP+c/RpT6JUkoaKrW1DOcw6lyXub8XtcNpI3xok6FnyNjS6ZbsrrtjTnS9yeZAQ==}
|
||||
|
||||
'@datadog/browser-core@6.33.0':
|
||||
resolution: {integrity: sha512-h/xuBuY4Xi32CbBHkChMMWlOQ4al/mFF08x3o0hqrderfJvX7VO5tD3OVNwF+pfMXOGlDtBCIhwazr9sn24h3g==}
|
||||
|
||||
'@datadog/browser-rum-core@6.33.0':
|
||||
resolution: {integrity: sha512-Ais4x7zQBrBI6JyILYwFAmjUsqy0iQp2C7JcEEfzU4YYYcVO3DGpyVuYYrqHT27jY1QToHY8EnQUr3Yaj6saDA==}
|
||||
|
||||
'@datadog/browser-rum@6.33.0':
|
||||
resolution: {integrity: sha512-zdxs1PyKt1RQ/fdxsmaCJUeBhB+leCIaPJBcum7uDi/DL8nuu9A9NUc5qGVPlY9zc12v7t2NI8bLpOrMjv+jTg==}
|
||||
peerDependencies:
|
||||
'@datadog/browser-logs': 6.33.0
|
||||
peerDependenciesMeta:
|
||||
'@datadog/browser-logs':
|
||||
optional: true
|
||||
|
||||
'@dimforge/rapier3d-compat@0.12.0':
|
||||
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
|
||||
|
||||
@@ -9791,6 +9811,17 @@ snapshots:
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@datadog/browser-core@6.33.0': {}
|
||||
|
||||
'@datadog/browser-rum-core@6.33.0':
|
||||
dependencies:
|
||||
'@datadog/browser-core': 6.33.0
|
||||
|
||||
'@datadog/browser-rum@6.33.0':
|
||||
dependencies:
|
||||
'@datadog/browser-core': 6.33.0
|
||||
'@datadog/browser-rum-core': 6.33.0
|
||||
|
||||
'@dimforge/rapier3d-compat@0.12.0': {}
|
||||
|
||||
'@dual-bundle/import-meta-resolve@4.2.1': {}
|
||||
|
||||
@@ -17,6 +17,7 @@ catalog:
|
||||
'@astrojs/vue': ^6.0.1
|
||||
'@comfyorg/comfyui-electron-types': 0.6.2
|
||||
'@customerio/cdp-analytics-browser': ^0.5.3
|
||||
'@datadog/browser-rum': ^6.33.0
|
||||
'@eslint/js': ^10.0.1
|
||||
'@formkit/auto-animate': ^0.9.0
|
||||
'@iconify-json/lucide': ^1.1.178
|
||||
|
||||
8
src/bootstrap.ts
Normal file
8
src/bootstrap.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
if (__DISTRIBUTION__ === 'cloud') {
|
||||
const { initDatadogRum } = await import('@/platform/telemetry/initDatadogRum')
|
||||
initDatadogRum()
|
||||
}
|
||||
|
||||
await import('./main')
|
||||
|
||||
export {}
|
||||
62
src/platform/telemetry/datadogRumBeforeSend.test.ts
Normal file
62
src/platform/telemetry/datadogRumBeforeSend.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn'
|
||||
import type { RumErrorEvent } from '@datadog/browser-rum'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { classifyRumErrorOrigin, rumBeforeSend } from './datadogRumBeforeSend'
|
||||
|
||||
function createErrorEvent(message: string, stack?: string): RumErrorEvent {
|
||||
return fromPartial<RumErrorEvent>({
|
||||
type: 'error',
|
||||
error: { message, source: 'source', stack }
|
||||
})
|
||||
}
|
||||
|
||||
describe('rumBeforeSend', () => {
|
||||
it('drops known third-party network noise', () => {
|
||||
const event = createErrorEvent(
|
||||
'Failed to fetch https://px.ads.linkedin.com/pixel'
|
||||
)
|
||||
|
||||
expect(rumBeforeSend(event, fromPartial({}))).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps application errors and tags their origin', () => {
|
||||
const event = createErrorEvent(
|
||||
'Application failed',
|
||||
'at render (https://cloud.comfy.org/assets/app.js:1:2)'
|
||||
)
|
||||
|
||||
expect(rumBeforeSend(event, fromPartial({}))).toBe(true)
|
||||
expect(event.context).toEqual({ error: { origin: 'first_party' } })
|
||||
})
|
||||
|
||||
it('tags custom extension errors with their folder', () => {
|
||||
const event = createErrorEvent(
|
||||
'Extension failed',
|
||||
'at run (https://cloud.comfy.org/extensions/comfyui-foo/main.js:1:2)'
|
||||
)
|
||||
|
||||
expect(rumBeforeSend(event, fromPartial({}))).toBe(true)
|
||||
expect(event.context).toEqual({
|
||||
error: { origin: 'extension', extension: 'comfyui-foo' }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('classifyRumErrorOrigin', () => {
|
||||
it('uses the first recognizable in-app stack frame', () => {
|
||||
const stack = [
|
||||
'at external (https://cdn.example.com/library.js:1:2)',
|
||||
'at cloud (https://cloud.comfy.org/extensions/cloud/main.js:1:2)',
|
||||
'at custom (https://cloud.comfy.org/extensions/comfyui-foo/main.js:1:2)'
|
||||
].join('\n')
|
||||
|
||||
expect(classifyRumErrorOrigin(stack)).toEqual({ origin: 'first_party' })
|
||||
})
|
||||
|
||||
it('classifies stacks without in-app frames as third party', () => {
|
||||
expect(
|
||||
classifyRumErrorOrigin('at external (https://cdn.example.com/app.js:1:2)')
|
||||
).toEqual({ origin: 'third_party' })
|
||||
})
|
||||
})
|
||||
72
src/platform/telemetry/datadogRumBeforeSend.ts
Normal file
72
src/platform/telemetry/datadogRumBeforeSend.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { RumBeforeSend, RumErrorEvent } from '@datadog/browser-rum'
|
||||
|
||||
const RUM_NOISE_HOSTS = [
|
||||
'facebook.com',
|
||||
'px.ads.linkedin.com',
|
||||
'browser-intake-us5-datadoghq.com',
|
||||
'e2.sy-d.io',
|
||||
'google-analytics.com',
|
||||
'googletagmanager.com'
|
||||
]
|
||||
|
||||
const FIRST_PARTY_EXTENSION_FOLDERS = new Set(['cloud', 'core'])
|
||||
|
||||
type RumErrorOrigin =
|
||||
| { origin: 'first_party' }
|
||||
| { origin: 'extension'; extension: string }
|
||||
| { origin: 'third_party' }
|
||||
|
||||
export function classifyRumErrorOrigin(stack?: string): RumErrorOrigin {
|
||||
if (!stack) return { origin: 'third_party' }
|
||||
|
||||
for (const line of stack.split('\n')) {
|
||||
const extensionFolder = /\/extensions\/([^/?#]+)\//.exec(line)?.[1]
|
||||
if (extensionFolder) {
|
||||
return FIRST_PARTY_EXTENSION_FOLDERS.has(extensionFolder)
|
||||
? { origin: 'first_party' }
|
||||
: { origin: 'extension', extension: extensionFolder }
|
||||
}
|
||||
|
||||
if (line.includes('/assets/')) return { origin: 'first_party' }
|
||||
}
|
||||
|
||||
return { origin: 'third_party' }
|
||||
}
|
||||
|
||||
function shouldKeepRumEvent(event: Parameters<RumBeforeSend>[0]): boolean {
|
||||
if (event.type !== 'error') return true
|
||||
|
||||
const message = event.error.message
|
||||
if (message.startsWith('intervention:')) return false
|
||||
if (message.includes('ResizeObserver loop')) return false
|
||||
|
||||
const isNetworkNoise =
|
||||
message.includes('csp_violation') || message.includes('Failed to fetch')
|
||||
return (
|
||||
!isNetworkNoise || !RUM_NOISE_HOSTS.some((host) => message.includes(host))
|
||||
)
|
||||
}
|
||||
|
||||
function tagRumErrorOrigin(event: RumErrorEvent): void {
|
||||
try {
|
||||
const errorOrigin = classifyRumErrorOrigin(event.error.stack)
|
||||
const existingErrorContext = event.context?.error
|
||||
const errorContext =
|
||||
typeof existingErrorContext === 'object' && existingErrorContext !== null
|
||||
? existingErrorContext
|
||||
: {}
|
||||
|
||||
event.context = {
|
||||
...event.context,
|
||||
error: { ...errorContext, ...errorOrigin }
|
||||
}
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export const rumBeforeSend: RumBeforeSend = (event) => {
|
||||
if (!shouldKeepRumEvent(event)) return false
|
||||
if (event.type === 'error') tagRumErrorOrigin(event)
|
||||
return true
|
||||
}
|
||||
55
src/platform/telemetry/initDatadogRum.test.ts
Normal file
55
src/platform/telemetry/initDatadogRum.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
getInitConfiguration: vi.fn(),
|
||||
init: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@datadog/browser-rum', () => ({
|
||||
datadogRum: hoisted
|
||||
}))
|
||||
|
||||
import { rumBeforeSend } from './datadogRumBeforeSend'
|
||||
import { initDatadogRum } from './initDatadogRum'
|
||||
|
||||
describe('initDatadogRum', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
hoisted.getInitConfiguration.mockReturnValue(undefined)
|
||||
})
|
||||
|
||||
it.for([
|
||||
{ hostname: 'cloud.comfy.org', env: 'prod-v2' },
|
||||
{ hostname: 'stagingcloud.comfy.org', env: 'stg-v2' },
|
||||
{ hostname: 'testcloud.comfy.org', env: 'test-v2' }
|
||||
])('initializes $hostname with the $env environment', ({ hostname, env }) => {
|
||||
initDatadogRum(hostname)
|
||||
|
||||
expect(hoisted.init).toHaveBeenCalledWith({
|
||||
clientToken: 'pub7704486e5b64eb4ff6f62891cda45559',
|
||||
applicationId: '041a9897-5516-4b1f-a245-1a9aa6895488',
|
||||
site: 'us5.datadoghq.com',
|
||||
service: 'comfy-cloud-frontend',
|
||||
env,
|
||||
version: __COMFYUI_FRONTEND_VERSION__,
|
||||
beforeSend: rumBeforeSend,
|
||||
sessionSampleRate: 100,
|
||||
sessionReplaySampleRate: 0,
|
||||
allowedTracingUrls: [expect.any(RegExp)]
|
||||
})
|
||||
})
|
||||
|
||||
it('does not initialize on an unknown hostname', () => {
|
||||
initDatadogRum('localhost')
|
||||
|
||||
expect(hoisted.init).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not initialize twice', () => {
|
||||
hoisted.getInitConfiguration.mockReturnValue({})
|
||||
|
||||
initDatadogRum('cloud.comfy.org')
|
||||
|
||||
expect(hoisted.init).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
27
src/platform/telemetry/initDatadogRum.ts
Normal file
27
src/platform/telemetry/initDatadogRum.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { datadogRum } from '@datadog/browser-rum'
|
||||
|
||||
import { rumBeforeSend } from './datadogRumBeforeSend'
|
||||
|
||||
const DATADOG_ENV_BY_HOSTNAME = new Map([
|
||||
['cloud.comfy.org', 'prod-v2'],
|
||||
['stagingcloud.comfy.org', 'stg-v2'],
|
||||
['testcloud.comfy.org', 'test-v2']
|
||||
])
|
||||
|
||||
export function initDatadogRum(hostname = window.location.hostname): void {
|
||||
const env = DATADOG_ENV_BY_HOSTNAME.get(hostname)
|
||||
if (!env || datadogRum.getInitConfiguration()) return
|
||||
|
||||
datadogRum.init({
|
||||
clientToken: 'pub7704486e5b64eb4ff6f62891cda45559',
|
||||
applicationId: '041a9897-5516-4b1f-a245-1a9aa6895488',
|
||||
site: 'us5.datadoghq.com',
|
||||
service: 'comfy-cloud-frontend',
|
||||
env,
|
||||
version: __COMFYUI_FRONTEND_VERSION__,
|
||||
beforeSend: rumBeforeSend,
|
||||
sessionSampleRate: 100,
|
||||
sessionReplaySampleRate: 0,
|
||||
allowedTracingUrls: [/^https:\/\/[^/]+\.comfy\.org/]
|
||||
})
|
||||
}
|
||||
23
src/services/extensionService.test.ts
Normal file
23
src/services/extensionService.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { shouldLoadExtension } from './extensionService'
|
||||
|
||||
describe('shouldLoadExtension', () => {
|
||||
it('skips the legacy Cloud RUM extension in cloud builds', () => {
|
||||
expect(shouldLoadExtension('/extensions/cloud/rum.js', true)).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the legacy path available outside cloud builds', () => {
|
||||
expect(shouldLoadExtension('/extensions/cloud/rum.js', false)).toBe(true)
|
||||
})
|
||||
|
||||
it('skips core extensions that load through the core entry point', () => {
|
||||
expect(shouldLoadExtension('/extensions/core/foo.js', false)).toBe(false)
|
||||
})
|
||||
|
||||
it('loads other extensions', () => {
|
||||
expect(shouldLoadExtension('/extensions/comfyui-foo/main.js', true)).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -15,6 +15,16 @@ import type { AuthUserInfo } from '@/types/authTypes'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
|
||||
const LEGACY_CLOUD_RUM_EXTENSION = '/extensions/cloud/rum.js'
|
||||
|
||||
export function shouldLoadExtension(
|
||||
extension: string,
|
||||
isCloudBuild: boolean
|
||||
): boolean {
|
||||
if (extension.includes('extensions/core')) return false
|
||||
return !isCloudBuild || extension !== LEGACY_CLOUD_RUM_EXTENSION
|
||||
}
|
||||
|
||||
export const useExtensionService = () => {
|
||||
const extensionStore = useExtensionStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -41,11 +51,8 @@ export const useExtensionService = () => {
|
||||
extensionStore.captureCoreExtensions()
|
||||
await Promise.all(
|
||||
extensions
|
||||
.filter((extension) => !extension.includes('extensions/core'))
|
||||
.filter(
|
||||
(extension) =>
|
||||
__DISTRIBUTION__ !== 'cloud' ||
|
||||
extension !== '/extensions/cloud/rum.js'
|
||||
.filter((extension) =>
|
||||
shouldLoadExtension(extension, __DISTRIBUTION__ === 'cloud')
|
||||
)
|
||||
.map(async (ext) => {
|
||||
try {
|
||||
|
||||
@@ -334,27 +334,6 @@ export default defineConfig({
|
||||
tailwindcss(),
|
||||
typegpuPlugin({}),
|
||||
comfyAPIPlugin(IS_DEV),
|
||||
{
|
||||
name: 'inject-cloud-rum',
|
||||
apply: 'build',
|
||||
transformIndexHtml(html) {
|
||||
if (DISTRIBUTION !== 'cloud') return html
|
||||
|
||||
return {
|
||||
html,
|
||||
tags: [
|
||||
{
|
||||
tag: 'script',
|
||||
attrs: {
|
||||
type: 'module',
|
||||
src: '/extensions/cloud/rum.js'
|
||||
},
|
||||
injectTo: 'head-prepend'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
// Exclude proprietary ABCROM fonts from non-cloud builds
|
||||
{
|
||||
name: 'exclude-proprietary-fonts',
|
||||
@@ -628,6 +607,11 @@ export default defineConfig({
|
||||
test: /[\\/]node_modules[\\/]@sentry[\\/]/,
|
||||
priority: 15
|
||||
},
|
||||
{
|
||||
name: 'vendor-datadog',
|
||||
test: /[\\/]node_modules[\\/]@datadog[\\/]/,
|
||||
priority: 15
|
||||
},
|
||||
|
||||
// UI component libraries
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user