mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-18 09:48:09 +00:00
Compare commits
4 Commits
agent/fix-
...
codex/clou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a73cef38e | ||
|
|
4e86bafed5 | ||
|
|
086cc313ef | ||
|
|
4c3c25bb59 |
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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 40 KiB |
@@ -1,9 +1,3 @@
|
||||
coverage:
|
||||
status:
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
|
||||
comment:
|
||||
layout: 'header, diff, flags, files'
|
||||
behavior: default
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -74,6 +74,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.6
|
||||
'@datadog/browser-rum':
|
||||
specifier: ^6.33.0
|
||||
version: 6.33.0
|
||||
'@eslint/js':
|
||||
specifier: ^10.0.1
|
||||
version: 10.0.1
|
||||
@@ -471,6 +474,9 @@ importers:
|
||||
'@customerio/cdp-analytics-browser':
|
||||
specifier: 'catalog:'
|
||||
version: 0.5.6
|
||||
'@datadog/browser-rum':
|
||||
specifier: 'catalog:'
|
||||
version: 6.33.0
|
||||
'@formkit/auto-animate':
|
||||
specifier: 'catalog:'
|
||||
version: 0.9.0
|
||||
@@ -1531,6 +1537,20 @@ packages:
|
||||
'@cyberalien/svg-utils@1.2.18':
|
||||
resolution: {integrity: sha512-avRdGj4MNoFyjehtaMhAggiceWmKRS5tB+P9YKZdcCATQsTYeiYifBjo0suyXOMIdadCcqxAZYniA305dpaMAA==}
|
||||
|
||||
'@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==}
|
||||
|
||||
@@ -10070,6 +10090,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 {}
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from 'reka-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { toValue } from 'vue'
|
||||
import type { StyleValue } from 'vue'
|
||||
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
@@ -20,12 +19,7 @@ defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
itemClass: string
|
||||
contentClass: string
|
||||
contentStyle?: StyleValue
|
||||
item: MenuItem
|
||||
}>()
|
||||
defineProps<{ itemClass: string; contentClass: string; item: MenuItem }>()
|
||||
</script>
|
||||
<template>
|
||||
<DropdownMenuSeparator
|
||||
@@ -43,7 +37,6 @@ defineProps<{
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent
|
||||
:class="contentClass"
|
||||
:style="contentStyle"
|
||||
:side-offset="2"
|
||||
:align-offset="-5"
|
||||
>
|
||||
@@ -53,7 +46,6 @@ defineProps<{
|
||||
:item="subitem"
|
||||
:item-class
|
||||
:content-class
|
||||
:content-style
|
||||
/>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
|
||||
@@ -72,7 +72,6 @@ const contentStyle = useModalLiftedZIndex(open)
|
||||
:key="toValue(item.label) ?? index"
|
||||
:item-class
|
||||
:content-class
|
||||
:content-style
|
||||
:item
|
||||
/>
|
||||
</slot>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ZIndex } from '@primeuix/utils/zindex'
|
||||
import { render, screen, within } from '@testing-library/vue'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
@@ -15,9 +14,9 @@ const i18n = createI18n({
|
||||
messages: { en: enMessages }
|
||||
})
|
||||
|
||||
function renderMenu(entries: MenuItem[] = [{ label: 'Item A' }]) {
|
||||
function renderMenu() {
|
||||
return render(DropdownMenu, {
|
||||
props: { entries },
|
||||
props: { entries: [{ label: 'Item A' }] },
|
||||
global: { plugins: [i18n], directives: { tooltip: {} } }
|
||||
})
|
||||
}
|
||||
@@ -54,32 +53,4 @@ describe('DropdownMenu z-index', () => {
|
||||
expect(menu.style.zIndex).toBe('')
|
||||
expect(menu.className).toContain('z-1700')
|
||||
})
|
||||
|
||||
it('opens a nested menu above a registered dialog', async () => {
|
||||
openModal = document.createElement('div')
|
||||
ZIndex.set('modal', openModal, 1700)
|
||||
const dialogZ = Number(openModal.style.zIndex)
|
||||
const command = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
renderMenu([
|
||||
{
|
||||
label: 'Change role',
|
||||
items: [
|
||||
{ label: 'Owner', command },
|
||||
{ label: 'Member', command }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
await user.click(screen.getByRole('button'))
|
||||
await user.click(screen.getByRole('menuitem', { name: 'Change role' }))
|
||||
|
||||
expect(await screen.findByRole('menuitem', { name: 'Owner' })).toBeVisible()
|
||||
expect(screen.getByRole('menuitem', { name: 'Member' })).toBeVisible()
|
||||
const submenu = screen
|
||||
.getAllByRole('menu')
|
||||
.find((menu) => within(menu).queryByRole('menuitem', { name: 'Owner' }))
|
||||
expect(submenu).toBeDefined()
|
||||
expect(Number(submenu?.style.zIndex)).toBeGreaterThan(dialogZ)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -833,44 +833,6 @@ describe('useLoad3d', () => {
|
||||
expect(composable.sceneConfig.value.backgroundImage).toBe('')
|
||||
expect(mockLoad3d.setBackgroundImage).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('should reset a leftover panorama mode to tiled when uploading a new image', async () => {
|
||||
vi.mocked(Load3dUtils.uploadFile).mockResolvedValue('uploaded-image.jpg')
|
||||
|
||||
const composable = useLoad3d(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await composable.initializeLoad3d(containerRef)
|
||||
composable.sceneConfig.value.backgroundRenderMode = 'panorama'
|
||||
await nextTick()
|
||||
|
||||
const file = new File([''], 'test.jpg', { type: 'image/jpeg' })
|
||||
await composable.handleBackgroundImageUpdate(file)
|
||||
|
||||
expect(composable.sceneConfig.value.backgroundRenderMode).toBe('tiled')
|
||||
await nextTick()
|
||||
expect(mockLoad3d.setBackgroundRenderMode).toHaveBeenLastCalledWith(
|
||||
'tiled'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not clear the background or touch render mode when the upload fails', async () => {
|
||||
vi.mocked(Load3dUtils.uploadFile).mockResolvedValue(undefined)
|
||||
|
||||
const composable = useLoad3d(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await composable.initializeLoad3d(containerRef)
|
||||
composable.sceneConfig.value.backgroundImage = 'existing.jpg'
|
||||
composable.sceneConfig.value.backgroundRenderMode = 'panorama'
|
||||
await nextTick()
|
||||
|
||||
const file = new File([''], 'test.jpg', { type: 'image/jpeg' })
|
||||
await composable.handleBackgroundImageUpdate(file)
|
||||
|
||||
expect(composable.sceneConfig.value.backgroundImage).toBe('existing.jpg')
|
||||
expect(composable.sceneConfig.value.backgroundRenderMode).toBe('panorama')
|
||||
})
|
||||
})
|
||||
|
||||
describe('model export', () => {
|
||||
|
||||
@@ -770,9 +770,6 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
: '3d'
|
||||
|
||||
const uploadedPath = await Load3dUtils.uploadFile(file, subfolder)
|
||||
if (!uploadedPath) return
|
||||
|
||||
sceneConfig.value.backgroundRenderMode = 'tiled'
|
||||
sceneConfig.value.backgroundImage = uploadedPath
|
||||
await load3d?.setBackgroundImage(uploadedPath)
|
||||
}
|
||||
|
||||
@@ -560,44 +560,6 @@ describe('useLoad3dViewer', () => {
|
||||
expect(viewer.hasBackgroundImage.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should reset render mode to tiled when uploading a new image', async () => {
|
||||
vi.mocked(Load3dUtils.uploadFile).mockResolvedValueOnce(
|
||||
'uploaded-image.jpg'
|
||||
)
|
||||
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d)
|
||||
|
||||
viewer.backgroundRenderMode.value = 'panorama'
|
||||
await nextTick()
|
||||
|
||||
const file = new File([''], 'test.jpg', { type: 'image/jpeg' })
|
||||
await viewer.handleBackgroundImageUpdate(file)
|
||||
|
||||
expect(viewer.backgroundRenderMode.value).toBe('tiled')
|
||||
})
|
||||
|
||||
it('should not clear the background or touch render mode when the upload fails', async () => {
|
||||
vi.mocked(Load3dUtils.uploadFile).mockResolvedValueOnce(undefined)
|
||||
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d)
|
||||
|
||||
viewer.backgroundImage.value = 'existing.jpg'
|
||||
viewer.backgroundRenderMode.value = 'panorama'
|
||||
await nextTick()
|
||||
|
||||
const file = new File([''], 'test.jpg', { type: 'image/jpeg' })
|
||||
await viewer.handleBackgroundImageUpdate(file)
|
||||
|
||||
expect(viewer.backgroundImage.value).toBe('existing.jpg')
|
||||
expect(viewer.backgroundRenderMode.value).toBe('panorama')
|
||||
})
|
||||
|
||||
it('should handle upload errors', async () => {
|
||||
vi.mocked(Load3dUtils.uploadFile).mockRejectedValueOnce(
|
||||
new Error('Upload failed')
|
||||
|
||||
@@ -752,7 +752,6 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
|
||||
)
|
||||
|
||||
if (uploadPath) {
|
||||
backgroundRenderMode.value = 'tiled'
|
||||
backgroundImage.value = uploadPath
|
||||
hasBackgroundImage.value = true
|
||||
}
|
||||
|
||||
@@ -353,10 +353,7 @@ describe('Load3d', () => {
|
||||
expect(sceneResize).toHaveBeenCalledWith(800, 400)
|
||||
})
|
||||
|
||||
function makeRenderMainSceneContext(
|
||||
backgroundType: 'color' | 'image' = 'color',
|
||||
activeCamera: THREE.Camera = ctx.cameraManager.activeCamera
|
||||
) {
|
||||
it('renderMainScene applies the letterboxed viewport and feeds aspect to the camera', () => {
|
||||
const setViewport = vi.fn()
|
||||
const setScissor = vi.fn()
|
||||
const setScissorTest = vi.fn()
|
||||
@@ -365,10 +362,6 @@ describe('Load3d', () => {
|
||||
const render = vi.fn()
|
||||
const updateAspectRatio = vi.fn()
|
||||
const renderBackground = vi.fn()
|
||||
const getCurrentBackgroundInfo = vi.fn(() => ({
|
||||
type: backgroundType,
|
||||
value: ''
|
||||
}))
|
||||
|
||||
const scene = {} as THREE.Scene
|
||||
|
||||
@@ -390,133 +383,19 @@ describe('Load3d', () => {
|
||||
targetHeight: 200,
|
||||
targetAspectRatio: 2,
|
||||
isViewerMode: false,
|
||||
cameraManager: {
|
||||
...ctx.cameraManager,
|
||||
updateAspectRatio,
|
||||
activeCamera
|
||||
},
|
||||
sceneManager: {
|
||||
...ctx.sceneManager,
|
||||
renderBackground,
|
||||
getCurrentBackgroundInfo,
|
||||
scene
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
setViewport,
|
||||
setScissor,
|
||||
setScissorTest,
|
||||
clear,
|
||||
render,
|
||||
updateAspectRatio,
|
||||
renderBackground,
|
||||
scene
|
||||
}
|
||||
}
|
||||
|
||||
it('renderMainScene renders the full canvas with an extrapolated view offset so the letterboxed rect is unchanged', () => {
|
||||
const r = makeRenderMainSceneContext()
|
||||
const camera = ctx.cameraManager.activeCamera as THREE.PerspectiveCamera
|
||||
|
||||
let viewAtRender: THREE.PerspectiveCamera['view'] = null
|
||||
r.render.mockImplementationOnce(() => {
|
||||
viewAtRender = camera.view ? { ...camera.view } : null
|
||||
cameraManager: { ...ctx.cameraManager, updateAspectRatio },
|
||||
sceneManager: { ...ctx.sceneManager, renderBackground, scene }
|
||||
})
|
||||
|
||||
ctx.load3d.renderMainScene()
|
||||
|
||||
// Container 800x600, target aspect 2:1 → letterboxed rect 800x400 at y=100
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(1, 0, 0, 800, 600)
|
||||
expect(r.setScissor).toHaveBeenNthCalledWith(1, 0, 0, 800, 600)
|
||||
expect(r.setScissorTest).toHaveBeenCalledWith(true)
|
||||
expect(r.clear).toHaveBeenCalledOnce()
|
||||
expect(r.updateAspectRatio).toHaveBeenCalledWith(2)
|
||||
expect(r.render).toHaveBeenNthCalledWith(1, r.scene, camera)
|
||||
|
||||
expect(viewAtRender).not.toBeNull()
|
||||
expect(viewAtRender!.enabled).toBe(true)
|
||||
expect(viewAtRender!.fullWidth).toBe(800)
|
||||
expect(viewAtRender!.fullHeight).toBe(400)
|
||||
expect(viewAtRender!.offsetX).toBeCloseTo(0)
|
||||
expect(viewAtRender!.offsetY).toBe(-100)
|
||||
expect(viewAtRender!.width).toBe(800)
|
||||
expect(viewAtRender!.height).toBe(600)
|
||||
|
||||
expect(camera.view?.enabled ?? false).toBe(false)
|
||||
})
|
||||
|
||||
it('renderMainScene dims the letterbox bars after rendering the scene', () => {
|
||||
const r = makeRenderMainSceneContext()
|
||||
|
||||
ctx.load3d.renderMainScene()
|
||||
|
||||
expect(r.render).toHaveBeenCalledTimes(3)
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(2, 0, 0, 800, 100)
|
||||
expect(r.setScissor).toHaveBeenNthCalledWith(2, 0, 0, 800, 100)
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(3, 0, 500, 800, 100)
|
||||
expect(r.setScissor).toHaveBeenNthCalledWith(3, 0, 500, 800, 100)
|
||||
})
|
||||
|
||||
it('renderMainScene keeps a color background covering the whole canvas', () => {
|
||||
const r = makeRenderMainSceneContext('color')
|
||||
|
||||
ctx.load3d.renderMainScene()
|
||||
|
||||
expect(r.renderBackground).toHaveBeenCalledWith()
|
||||
// No viewport narrowing before the bars are dimmed.
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(2, 0, 0, 800, 100)
|
||||
})
|
||||
|
||||
it('renderMainScene confines an image background to the letterboxed rect', () => {
|
||||
const r = makeRenderMainSceneContext('image')
|
||||
|
||||
ctx.load3d.renderMainScene()
|
||||
|
||||
// Viewport/scissor narrow to the letterbox rect for the background
|
||||
// pass, then restore to the full canvas for the scene pass.
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(2, 0, 100, 800, 400)
|
||||
expect(r.setScissor).toHaveBeenNthCalledWith(2, 0, 100, 800, 400)
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(3, 0, 0, 800, 600)
|
||||
expect(r.setScissor).toHaveBeenNthCalledWith(3, 0, 0, 800, 600)
|
||||
|
||||
const backgroundOrder = r.renderBackground.mock.invocationCallOrder[0]
|
||||
expect(backgroundOrder).toBeGreaterThan(
|
||||
r.setViewport.mock.invocationCallOrder[1]
|
||||
)
|
||||
expect(backgroundOrder).toBeLessThan(
|
||||
r.setViewport.mock.invocationCallOrder[2]
|
||||
)
|
||||
})
|
||||
|
||||
it('renderMainScene applies the view offset to orthographic cameras too', () => {
|
||||
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1)
|
||||
const r = makeRenderMainSceneContext('color', camera)
|
||||
|
||||
let viewEnabledAtRender = false
|
||||
r.render.mockImplementationOnce(() => {
|
||||
viewEnabledAtRender = camera.view?.enabled ?? false
|
||||
})
|
||||
|
||||
ctx.load3d.renderMainScene()
|
||||
|
||||
expect(r.render).toHaveBeenNthCalledWith(1, r.scene, camera)
|
||||
expect(viewEnabledAtRender).toBe(true)
|
||||
expect(camera.view?.enabled ?? false).toBe(false)
|
||||
})
|
||||
|
||||
it('renderMainScene falls back to the letterboxed viewport for cameras without view-offset support', () => {
|
||||
const camera = new THREE.Camera()
|
||||
const r = makeRenderMainSceneContext('color', camera)
|
||||
|
||||
ctx.load3d.renderMainScene()
|
||||
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(1, 0, 0, 800, 600)
|
||||
expect(r.setViewport).toHaveBeenNthCalledWith(2, 0, 100, 800, 400)
|
||||
expect(r.setScissor).toHaveBeenNthCalledWith(2, 0, 100, 800, 400)
|
||||
expect(r.render).toHaveBeenCalledTimes(1)
|
||||
expect(r.render).toHaveBeenCalledWith(r.scene, camera)
|
||||
expect(r.renderBackground).toHaveBeenCalledWith()
|
||||
expect(setViewport).toHaveBeenNthCalledWith(1, 0, 0, 800, 600)
|
||||
expect(setScissor).toHaveBeenNthCalledWith(1, 0, 0, 800, 600)
|
||||
expect(setViewport).toHaveBeenNthCalledWith(2, 0, 100, 800, 400)
|
||||
expect(setScissor).toHaveBeenNthCalledWith(2, 0, 100, 800, 400)
|
||||
expect(updateAspectRatio).toHaveBeenCalledWith(2)
|
||||
expect(setScissorTest).toHaveBeenCalledWith(true)
|
||||
expect(render).toHaveBeenCalledWith(scene, ctx.cameraManager.activeCamera)
|
||||
})
|
||||
|
||||
it('setBackgroundImage updates background size with letterbox dimensions when a texture is loaded', async () => {
|
||||
|
||||
@@ -13,7 +13,6 @@ type CameraStub = {
|
||||
getCurrentCameraType: ReturnType<typeof vi.fn>
|
||||
handleResize: ReturnType<typeof vi.fn>
|
||||
updateAspectRatio: ReturnType<typeof vi.fn>
|
||||
dispose: ReturnType<typeof vi.fn>
|
||||
activeCamera: THREE.Camera
|
||||
}
|
||||
|
||||
@@ -49,7 +48,6 @@ function makeViewportInstance() {
|
||||
getCurrentCameraType: vi.fn(() => 'perspective' as const),
|
||||
handleResize: vi.fn(),
|
||||
updateAspectRatio: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
activeCamera: new THREE.PerspectiveCamera()
|
||||
}
|
||||
const sceneManager: SceneStub = {
|
||||
@@ -449,82 +447,6 @@ describe('Viewport3d', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('letterbox dimmer lifecycle', () => {
|
||||
type DimmerInternals = {
|
||||
dimLetterboxBars(
|
||||
bars: { x: number; y: number; width: number; height: number }[]
|
||||
): void
|
||||
disposeManagers(): void
|
||||
letterboxDimmer: {
|
||||
scene: THREE.Scene
|
||||
camera: THREE.OrthographicCamera
|
||||
geometry: THREE.PlaneGeometry
|
||||
material: THREE.MeshBasicMaterial
|
||||
} | null
|
||||
}
|
||||
|
||||
it('does not create the dim overlay when there are no letterbox bars', () => {
|
||||
const render = vi.fn()
|
||||
Object.assign(ctx.viewport, {
|
||||
view: {
|
||||
renderer: { setViewport: vi.fn(), setScissor: vi.fn(), render }
|
||||
}
|
||||
})
|
||||
const internals = ctx.viewport as unknown as DimmerInternals
|
||||
|
||||
internals.dimLetterboxBars([])
|
||||
|
||||
expect(render).not.toHaveBeenCalled()
|
||||
expect(internals.letterboxDimmer ?? null).toBeNull()
|
||||
})
|
||||
|
||||
it('renders each bar with a lazily created dim overlay', () => {
|
||||
const setViewport = vi.fn()
|
||||
const setScissor = vi.fn()
|
||||
const render = vi.fn()
|
||||
Object.assign(ctx.viewport, {
|
||||
view: { renderer: { setViewport, setScissor, render } }
|
||||
})
|
||||
const internals = ctx.viewport as unknown as DimmerInternals
|
||||
|
||||
internals.dimLetterboxBars([
|
||||
{ x: 0, y: 0, width: 100, height: 20 },
|
||||
{ x: 0, y: 80, width: 100, height: 20 }
|
||||
])
|
||||
|
||||
const dimmer = internals.letterboxDimmer
|
||||
expect(dimmer).not.toBeNull()
|
||||
expect(setViewport).toHaveBeenNthCalledWith(1, 0, 0, 100, 20)
|
||||
expect(setViewport).toHaveBeenNthCalledWith(2, 0, 80, 100, 20)
|
||||
expect(render).toHaveBeenCalledTimes(2)
|
||||
expect(render).toHaveBeenCalledWith(dimmer!.scene, dimmer!.camera)
|
||||
})
|
||||
|
||||
it('disposeManagers disposes the dim overlay resources', () => {
|
||||
Object.assign(ctx.viewport, {
|
||||
view: {
|
||||
renderer: {
|
||||
setViewport: vi.fn(),
|
||||
setScissor: vi.fn(),
|
||||
render: vi.fn()
|
||||
}
|
||||
}
|
||||
})
|
||||
const internals = ctx.viewport as unknown as DimmerInternals
|
||||
|
||||
internals.dimLetterboxBars([{ x: 0, y: 0, width: 100, height: 20 }])
|
||||
const dimmer = internals.letterboxDimmer!
|
||||
const geometryDispose = vi.spyOn(dimmer.geometry, 'dispose')
|
||||
const materialDispose = vi.spyOn(dimmer.material, 'dispose')
|
||||
|
||||
internals.disposeManagers()
|
||||
|
||||
expect(geometryDispose).toHaveBeenCalledOnce()
|
||||
expect(materialDispose).toHaveBeenCalledOnce()
|
||||
expect(internals.letterboxDimmer).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('start / remove lifecycle', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
@@ -18,52 +18,13 @@ import type {
|
||||
import { attachContextMenuGuard } from './load3dContextMenuGuard'
|
||||
import type { RenderLoopHandle } from './load3dRenderLoop'
|
||||
import { startRenderLoop } from './load3dRenderLoop'
|
||||
import type {
|
||||
LetterboxedViewport,
|
||||
LetterboxNdc,
|
||||
ViewportRect
|
||||
} from './load3dViewport'
|
||||
import type { LetterboxNdc } from './load3dViewport'
|
||||
import {
|
||||
clientPointToLetterboxNdc,
|
||||
computeLetterboxBars,
|
||||
computeLetterboxedViewport,
|
||||
isLoad3dActive
|
||||
} from './load3dViewport'
|
||||
|
||||
const LETTERBOX_CLEAR_COLOR = 0x0a0a0a
|
||||
const LETTERBOX_DIM_OPACITY = 0.5
|
||||
|
||||
type LetterboxDimmer = {
|
||||
scene: THREE.Scene
|
||||
camera: THREE.OrthographicCamera
|
||||
geometry: THREE.PlaneGeometry
|
||||
material: THREE.MeshBasicMaterial
|
||||
}
|
||||
|
||||
function supportsViewOffset(
|
||||
camera: THREE.Camera
|
||||
): camera is THREE.PerspectiveCamera | THREE.OrthographicCamera {
|
||||
return (
|
||||
camera instanceof THREE.PerspectiveCamera ||
|
||||
camera instanceof THREE.OrthographicCamera
|
||||
)
|
||||
}
|
||||
|
||||
function createLetterboxDimmer(): LetterboxDimmer {
|
||||
const geometry = new THREE.PlaneGeometry(2, 2)
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: 0x000000,
|
||||
transparent: true,
|
||||
opacity: LETTERBOX_DIM_OPACITY,
|
||||
depthTest: false,
|
||||
depthWrite: false
|
||||
})
|
||||
const scene = new THREE.Scene()
|
||||
scene.add(new THREE.Mesh(geometry, material))
|
||||
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1)
|
||||
return { scene, camera, geometry, material }
|
||||
}
|
||||
|
||||
const VIEW_HELPER_SIZE = 128
|
||||
|
||||
export type Viewport3dDeps = {
|
||||
@@ -106,7 +67,6 @@ export class Viewport3d {
|
||||
private overlay: SceneOverlay | null = null
|
||||
private initialRenderTimer: ReturnType<typeof setTimeout> | null = null
|
||||
private viewPixelScale = 1
|
||||
private letterboxDimmer: LetterboxDimmer | null = null
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
@@ -297,96 +257,29 @@ export class Viewport3d {
|
||||
this.renderer.setScissor(0, 0, viewWidth, viewHeight)
|
||||
this.renderer.setScissorTest(true)
|
||||
|
||||
if (!this.shouldMaintainAspectRatio()) {
|
||||
if (this.shouldMaintainAspectRatio()) {
|
||||
const { offsetX, offsetY, width, height } = computeLetterboxedViewport(
|
||||
{ width: viewWidth, height: viewHeight },
|
||||
this.targetAspectRatio
|
||||
)
|
||||
|
||||
this.renderer.setClearColor(0x0a0a0a)
|
||||
this.renderer.clear()
|
||||
|
||||
this.renderer.setViewport(offsetX, offsetY, width, height)
|
||||
this.renderer.setScissor(offsetX, offsetY, width, height)
|
||||
|
||||
this.cameraManager.updateAspectRatio(width / height)
|
||||
} else {
|
||||
this.renderer.setClearColor(
|
||||
this.view.state.clearColor,
|
||||
this.view.state.clearAlpha
|
||||
)
|
||||
this.renderer.clear()
|
||||
this.sceneManager.renderBackground()
|
||||
this.renderer.render(this.sceneManager.scene, this.getRenderCamera())
|
||||
return
|
||||
}
|
||||
|
||||
const container = { width: viewWidth, height: viewHeight }
|
||||
const viewport = computeLetterboxedViewport(
|
||||
container,
|
||||
this.targetAspectRatio
|
||||
)
|
||||
|
||||
this.renderer.setClearColor(LETTERBOX_CLEAR_COLOR)
|
||||
this.renderer.clear()
|
||||
|
||||
this.cameraManager.updateAspectRatio(viewport.width / viewport.height)
|
||||
|
||||
const camera = this.getRenderCamera()
|
||||
|
||||
if (!supportsViewOffset(camera)) {
|
||||
this.renderer.setViewport(
|
||||
viewport.offsetX,
|
||||
viewport.offsetY,
|
||||
viewport.width,
|
||||
viewport.height
|
||||
)
|
||||
this.renderer.setScissor(
|
||||
viewport.offsetX,
|
||||
viewport.offsetY,
|
||||
viewport.width,
|
||||
viewport.height
|
||||
)
|
||||
this.sceneManager.renderBackground()
|
||||
this.renderer.render(this.sceneManager.scene, camera)
|
||||
return
|
||||
}
|
||||
|
||||
camera.setViewOffset(
|
||||
viewport.width,
|
||||
viewport.height,
|
||||
-viewport.offsetX,
|
||||
-viewport.offsetY,
|
||||
viewWidth,
|
||||
viewHeight
|
||||
)
|
||||
|
||||
this.renderLetterboxedBackground(viewport)
|
||||
this.renderer.render(this.sceneManager.scene, camera)
|
||||
camera.clearViewOffset()
|
||||
|
||||
this.dimLetterboxBars(computeLetterboxBars(container, viewport))
|
||||
}
|
||||
|
||||
private renderLetterboxedBackground(viewport: LetterboxedViewport): void {
|
||||
if (this.sceneManager.getCurrentBackgroundInfo().type !== 'image') {
|
||||
this.sceneManager.renderBackground()
|
||||
return
|
||||
}
|
||||
|
||||
this.renderer.setViewport(
|
||||
viewport.offsetX,
|
||||
viewport.offsetY,
|
||||
viewport.width,
|
||||
viewport.height
|
||||
)
|
||||
this.renderer.setScissor(
|
||||
viewport.offsetX,
|
||||
viewport.offsetY,
|
||||
viewport.width,
|
||||
viewport.height
|
||||
)
|
||||
this.sceneManager.renderBackground()
|
||||
this.renderer.setViewport(0, 0, this.view.width, this.view.height)
|
||||
this.renderer.setScissor(0, 0, this.view.width, this.view.height)
|
||||
}
|
||||
|
||||
private dimLetterboxBars(bars: ViewportRect[]): void {
|
||||
if (bars.length === 0) return
|
||||
|
||||
const dimmer = (this.letterboxDimmer ??= createLetterboxDimmer())
|
||||
for (const bar of bars) {
|
||||
this.renderer.setViewport(bar.x, bar.y, bar.width, bar.height)
|
||||
this.renderer.setScissor(bar.x, bar.y, bar.width, bar.height)
|
||||
this.renderer.render(dimmer.scene, dimmer.camera)
|
||||
}
|
||||
this.renderer.render(this.sceneManager.scene, this.getRenderCamera())
|
||||
}
|
||||
|
||||
clientPointToNdc(clientX: number, clientY: number): LetterboxNdc | null {
|
||||
@@ -542,11 +435,6 @@ export class Viewport3d {
|
||||
this.overlay.dispose()
|
||||
this.overlay = null
|
||||
}
|
||||
if (this.letterboxDimmer) {
|
||||
this.letterboxDimmer.geometry.dispose()
|
||||
this.letterboxDimmer.material.dispose()
|
||||
this.letterboxDimmer = null
|
||||
}
|
||||
this.sceneManager.dispose()
|
||||
this.cameraManager.dispose()
|
||||
this.controlsManager.dispose()
|
||||
|
||||
@@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
clientPointToLetterboxNdc,
|
||||
computeLetterboxBars,
|
||||
computeLetterboxedViewport,
|
||||
isLoad3dActive
|
||||
} from './load3dViewport'
|
||||
@@ -83,53 +82,6 @@ describe('computeLetterboxedViewport', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeLetterboxBars', () => {
|
||||
it('returns left and right bars for a pillarboxed viewport', () => {
|
||||
const container = { width: 800, height: 400 }
|
||||
const bars = computeLetterboxBars(
|
||||
container,
|
||||
computeLetterboxedViewport(container, 1)
|
||||
)
|
||||
|
||||
expect(bars).toEqual([
|
||||
{ x: 0, y: 0, width: 200, height: 400 },
|
||||
{ x: 600, y: 0, width: 200, height: 400 }
|
||||
])
|
||||
})
|
||||
|
||||
it('returns bottom and top bars for a letterboxed viewport', () => {
|
||||
const container = { width: 400, height: 800 }
|
||||
const bars = computeLetterboxBars(
|
||||
container,
|
||||
computeLetterboxedViewport(container, 1)
|
||||
)
|
||||
|
||||
expect(bars).toEqual([
|
||||
{ x: 0, y: 0, width: 400, height: 200 },
|
||||
{ x: 0, y: 600, width: 400, height: 200 }
|
||||
])
|
||||
})
|
||||
|
||||
it('returns no bars when the viewport fills the container', () => {
|
||||
const container = { width: 1024, height: 768 }
|
||||
const bars = computeLetterboxBars(
|
||||
container,
|
||||
computeLetterboxedViewport(container, 1024 / 768)
|
||||
)
|
||||
|
||||
expect(bars).toEqual([])
|
||||
})
|
||||
|
||||
it('ignores sub-pixel offsets', () => {
|
||||
const bars = computeLetterboxBars(
|
||||
{ width: 800, height: 400.5 },
|
||||
{ offsetX: 0, offsetY: 0.25, width: 800, height: 400 }
|
||||
)
|
||||
|
||||
expect(bars).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('isLoad3dActive', () => {
|
||||
const idle: Load3dActivityFlags = {
|
||||
mouseOnNode: false,
|
||||
|
||||
@@ -2,20 +2,13 @@ import { denormalize, normalize } from '@/utils/mathUtil'
|
||||
|
||||
type Size = { width: number; height: number }
|
||||
|
||||
export type LetterboxedViewport = {
|
||||
type LetterboxedViewport = {
|
||||
offsetX: number
|
||||
offsetY: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export type ViewportRect = {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export function computeLetterboxedViewport(
|
||||
container: Size,
|
||||
targetAspectRatio: number
|
||||
@@ -76,37 +69,6 @@ export function clientPointToLetterboxNdc(
|
||||
)
|
||||
}
|
||||
|
||||
export function computeLetterboxBars(
|
||||
container: Size,
|
||||
viewport: LetterboxedViewport
|
||||
): ViewportRect[] {
|
||||
if (viewport.offsetX >= 1) {
|
||||
return [
|
||||
{ x: 0, y: 0, width: viewport.offsetX, height: container.height },
|
||||
{
|
||||
x: viewport.offsetX + viewport.width,
|
||||
y: 0,
|
||||
width: container.width - viewport.offsetX - viewport.width,
|
||||
height: container.height
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (viewport.offsetY >= 1) {
|
||||
return [
|
||||
{ x: 0, y: 0, width: container.width, height: viewport.offsetY },
|
||||
{
|
||||
x: 0,
|
||||
y: viewport.offsetY + viewport.height,
|
||||
width: container.width,
|
||||
height: container.height - viewport.offsetY - viewport.height
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export type Load3dActivityFlags = {
|
||||
mouseOnNode: boolean
|
||||
mouseOnScene: boolean
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { KeybindingImpl } from '@/platform/keybindings/keybinding'
|
||||
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
||||
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
const settings = vi.hoisted(() => ({
|
||||
values: {} as Record<string, unknown>
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn((key: string) => settings.values[key] ?? [])
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('keybindingService - registerUserKeybindings', () => {
|
||||
let warnSpy: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
settings.values = {}
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('does not warn when unset binding targets a command that no longer exists', () => {
|
||||
// A command removed from the app (e.g. ConvertSelectedNodesToGroupNode,
|
||||
// removed in #12931) can still linger in the persisted UnsetBindings.
|
||||
settings.values['Comfy.Keybinding.UnsetBindings'] = [
|
||||
{
|
||||
commandId: 'ConvertSelectedNodesToGroupNode',
|
||||
combo: { key: 'g', ctrl: true, alt: false, shift: false }
|
||||
}
|
||||
]
|
||||
|
||||
useKeybindingService().registerUserKeybindings()
|
||||
|
||||
expect(warnSpy).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('Trying to unset non-exist keybinding')
|
||||
)
|
||||
})
|
||||
|
||||
it('still unsets bindings for commands that are registered', () => {
|
||||
const commandStore = useCommandStore()
|
||||
commandStore.registerCommand({
|
||||
id: 'Comfy.Test.Registered',
|
||||
function: vi.fn()
|
||||
})
|
||||
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const combo = { key: 'g', ctrl: true, alt: false, shift: false }
|
||||
keybindingStore.addDefaultKeybinding(
|
||||
new KeybindingImpl({ commandId: 'Comfy.Test.Registered', combo })
|
||||
)
|
||||
|
||||
settings.values['Comfy.Keybinding.UnsetBindings'] = [
|
||||
{ commandId: 'Comfy.Test.Registered', combo }
|
||||
]
|
||||
|
||||
useKeybindingService().registerUserKeybindings()
|
||||
|
||||
expect(warnSpy).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('Trying to unset non-exist keybinding')
|
||||
)
|
||||
expect(
|
||||
keybindingStore.getKeybindingByCommandId('Comfy.Test.Registered')
|
||||
).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -121,9 +121,6 @@ export function useKeybindingService() {
|
||||
function registerUserKeybindings() {
|
||||
const unsetBindings = settingStore.get('Comfy.Keybinding.UnsetBindings')
|
||||
for (const keybinding of unsetBindings) {
|
||||
if (!commandStore.isRegistered(keybinding.commandId)) {
|
||||
continue
|
||||
}
|
||||
keybindingStore.unsetKeybinding(new KeybindingImpl(keybinding))
|
||||
}
|
||||
const newBindings = settingStore.get('Comfy.Keybinding.NewBindings')
|
||||
|
||||
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
|
||||
}
|
||||
61
src/platform/telemetry/initDatadogRum.test.ts
Normal file
61
src/platform/telemetry/initDatadogRum.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
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' },
|
||||
{ hostname: 'fe-pr-13691.testenvs.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.for([
|
||||
'localhost',
|
||||
'testenvs.comfy.org',
|
||||
'eviltestenvs.comfy.org',
|
||||
'preview.testenvs.comfy.org.example.com'
|
||||
])('does not initialize on unknown hostname %s', (hostname) => {
|
||||
initDatadogRum(hostname)
|
||||
|
||||
expect(hoisted.init).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not initialize twice', () => {
|
||||
hoisted.getInitConfiguration.mockReturnValue({})
|
||||
|
||||
initDatadogRum('cloud.comfy.org')
|
||||
|
||||
expect(hoisted.init).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
29
src/platform/telemetry/initDatadogRum.ts
Normal file
29
src/platform/telemetry/initDatadogRum.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
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) ??
|
||||
(hostname.endsWith('.testenvs.comfy.org') ? 'test-v2' : undefined)
|
||||
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/]
|
||||
})
|
||||
}
|
||||
@@ -534,7 +534,7 @@ function handleSubscribe(tierKey: CheckoutTierKey) {
|
||||
}
|
||||
|
||||
function handleContactUs() {
|
||||
window.open('https://discord.com/invite/comfyorg', '_blank')
|
||||
window.open('https://www.comfy.org/discord', '_blank')
|
||||
}
|
||||
|
||||
function handleViewEnterprise() {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<i18n-t keypath="subscription.preview.termsAgreement" tag="span">
|
||||
<template #terms>
|
||||
<a
|
||||
href="https://www.comfy.org/terms-of-service"
|
||||
href="https://www.comfy.org/terms"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="underline hover:text-base-foreground"
|
||||
@@ -13,7 +13,7 @@
|
||||
</template>
|
||||
<template #privacy>
|
||||
<a
|
||||
href="https://www.comfy.org/privacy-policy"
|
||||
href="https://www.comfy.org/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="underline hover:text-base-foreground"
|
||||
|
||||
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,7 +51,9 @@ export const useExtensionService = () => {
|
||||
extensionStore.captureCoreExtensions()
|
||||
await Promise.all(
|
||||
extensions
|
||||
.filter((extension) => !extension.includes('extensions/core'))
|
||||
.filter((extension) =>
|
||||
shouldLoadExtension(extension, __DISTRIBUTION__ === 'cloud')
|
||||
)
|
||||
.map(async (ext) => {
|
||||
try {
|
||||
await import(/* @vite-ignore */ api.fileURL(ext))
|
||||
|
||||
@@ -607,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