fix: own Cloud RUM initialization in frontend

This commit is contained in:
huang47
2026-07-17 15:05:20 -07:00
parent 4c3c25bb59
commit 086cc313ef
13 changed files with 316 additions and 27 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -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
View File

@@ -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': {}

View File

@@ -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
View File

@@ -0,0 +1,8 @@
if (__DISTRIBUTION__ === 'cloud') {
const { initDatadogRum } = await import('@/platform/telemetry/initDatadogRum')
initDatadogRum()
}
await import('./main')
export {}

View 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' })
})
})

View 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
}

View 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()
})
})

View 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/]
})
}

View 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
)
})
})

View File

@@ -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 {

View File

@@ -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
{