mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-17 09:18:26 +00:00
feat: apply stable names to automatic RUM views
This commit is contained in:
@@ -10,6 +10,7 @@ import type {
|
||||
ShareFlowMetadata,
|
||||
ShareLinkOpenedMetadata,
|
||||
ExecutionErrorMetadata,
|
||||
ExecutionStartMetadata,
|
||||
ExecutionSuccessMetadata,
|
||||
HelpCenterClosedMetadata,
|
||||
HelpCenterOpenedMetadata,
|
||||
@@ -268,6 +269,10 @@ export class TelemetryRegistry implements TelemetryDispatcher {
|
||||
this.dispatch((provider) => provider.trackWorkflowExecution?.())
|
||||
}
|
||||
|
||||
trackExecutionStarted(metadata: ExecutionStartMetadata): void {
|
||||
this.dispatch((provider) => provider.trackExecutionStarted?.(metadata))
|
||||
}
|
||||
|
||||
trackExecutionError(metadata: ExecutionErrorMetadata): void {
|
||||
this.dispatch((provider) => provider.trackExecutionError?.(metadata))
|
||||
}
|
||||
|
||||
@@ -3,47 +3,24 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DatadogRumTelemetryProvider } from './DatadogRumTelemetryProvider'
|
||||
|
||||
const addAction = vi.fn()
|
||||
const setViewContextProperty = vi.fn()
|
||||
const getInternalContext = vi.fn()
|
||||
const setViewName = vi.fn()
|
||||
|
||||
function installDatadogRum(): void {
|
||||
Object.defineProperty(window, 'DD_RUM', {
|
||||
configurable: true,
|
||||
value: { addAction, setViewContextProperty }
|
||||
value: { addAction, getInternalContext, setViewName }
|
||||
})
|
||||
}
|
||||
|
||||
function setNavigationType(type?: NavigationTimingType): void {
|
||||
vi.spyOn(performance, 'getEntriesByType').mockReturnValue(
|
||||
type ? ([{ type }] as PerformanceNavigationTiming[]) : []
|
||||
)
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
addAction.mockReset()
|
||||
setViewContextProperty.mockReset()
|
||||
vi.restoreAllMocks()
|
||||
getInternalContext.mockReset()
|
||||
setViewName.mockReset()
|
||||
Reflect.deleteProperty(window, 'DD_RUM')
|
||||
})
|
||||
|
||||
describe('DatadogRumTelemetryProvider', () => {
|
||||
it.for([
|
||||
{ expected: 'navigate', type: 'navigate' },
|
||||
{ expected: 'reload', type: 'reload' },
|
||||
{ expected: 'back_forward', type: 'back_forward' },
|
||||
{ expected: 'prerender', type: 'prerender' },
|
||||
{ expected: 'unknown', type: undefined }
|
||||
] as const)('sets $expected navigation context', ({ expected, type }) => {
|
||||
installDatadogRum()
|
||||
setNavigationType(type)
|
||||
|
||||
new DatadogRumTelemetryProvider()
|
||||
|
||||
expect(setViewContextProperty).toHaveBeenCalledWith(
|
||||
'navigation_type',
|
||||
expected
|
||||
)
|
||||
})
|
||||
|
||||
it.for([
|
||||
{ expected: 'workspace', path: 'https://cloud.comfy.org/' },
|
||||
{ expected: 'account_access', path: 'https://cloud.comfy.org/cloud/' },
|
||||
@@ -68,65 +45,73 @@ describe('DatadogRumTelemetryProvider', () => {
|
||||
expected: 'support_recovery',
|
||||
path: 'https://cloud.comfy.org/cloud/auth-timeout'
|
||||
}
|
||||
] as const)('sets $expected product surface', ({ expected, path }) => {
|
||||
] as const)('names the current view $expected', ({ expected, path }) => {
|
||||
installDatadogRum()
|
||||
setNavigationType('navigate')
|
||||
const provider = new DatadogRumTelemetryProvider()
|
||||
|
||||
provider.trackPageView('ignored', { path })
|
||||
new DatadogRumTelemetryProvider().trackPageView('ignored', { path })
|
||||
|
||||
expect(setViewContextProperty).toHaveBeenLastCalledWith(
|
||||
'product_surface',
|
||||
expected
|
||||
)
|
||||
expect(setViewName).toHaveBeenCalledWith(expected)
|
||||
})
|
||||
|
||||
it('tracks workflow execution starts', () => {
|
||||
it('tracks workflow execution starts with the originating view', () => {
|
||||
installDatadogRum()
|
||||
setNavigationType('navigate')
|
||||
getInternalContext.mockReturnValue({ view: { id: 'view-a' } })
|
||||
|
||||
new DatadogRumTelemetryProvider().trackWorkflowExecution()
|
||||
new DatadogRumTelemetryProvider().trackExecutionStarted({
|
||||
jobId: 'job-a',
|
||||
startTime: 42
|
||||
})
|
||||
|
||||
expect(getInternalContext).toHaveBeenCalledWith(42)
|
||||
expect(addAction).toHaveBeenCalledWith('workflow_execution_started', {
|
||||
product: 'cloud_generation',
|
||||
product_surface: 'workspace'
|
||||
origin_view_id: 'view-a'
|
||||
})
|
||||
})
|
||||
|
||||
it.for([
|
||||
{
|
||||
outcome: 'success',
|
||||
trackOutcome: (provider: DatadogRumTelemetryProvider) =>
|
||||
provider.trackExecutionSuccess()
|
||||
},
|
||||
{
|
||||
outcome: 'failure',
|
||||
trackOutcome: (provider: DatadogRumTelemetryProvider) =>
|
||||
provider.trackExecutionError()
|
||||
}
|
||||
] as const)(
|
||||
'tracks workflow $outcome outcomes',
|
||||
({ outcome, trackOutcome }) => {
|
||||
installDatadogRum()
|
||||
setNavigationType('navigate')
|
||||
const provider = new DatadogRumTelemetryProvider()
|
||||
it('preserves each origin view across workflow switches', () => {
|
||||
installDatadogRum()
|
||||
getInternalContext
|
||||
.mockReturnValueOnce({ view: { id: 'view-a' } })
|
||||
.mockReturnValueOnce({ view: { id: 'view-b' } })
|
||||
const provider = new DatadogRumTelemetryProvider()
|
||||
|
||||
trackOutcome(provider)
|
||||
provider.trackExecutionStarted({ jobId: 'job-a', startTime: 1 })
|
||||
provider.trackExecutionStarted({ jobId: 'job-b', startTime: 2 })
|
||||
provider.trackExecutionSuccess({ jobId: 'job-a' })
|
||||
provider.trackExecutionError({ jobId: 'job-b' })
|
||||
|
||||
expect(addAction).toHaveBeenCalledWith('workflow_execution_completed', {
|
||||
outcome,
|
||||
product: 'cloud_generation',
|
||||
product_surface: 'workspace'
|
||||
})
|
||||
}
|
||||
)
|
||||
expect(addAction).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'workflow_execution_completed',
|
||||
{
|
||||
outcome: 'success',
|
||||
origin_view_id: 'view-a',
|
||||
product: 'cloud_generation'
|
||||
}
|
||||
)
|
||||
expect(addAction).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'workflow_execution_completed',
|
||||
{
|
||||
outcome: 'failure',
|
||||
origin_view_id: 'view-b',
|
||||
product: 'cloud_generation'
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('does nothing when Datadog RUM is unavailable', () => {
|
||||
const provider = new DatadogRumTelemetryProvider()
|
||||
|
||||
expect(() => provider.trackWorkflowExecution()).not.toThrow()
|
||||
expect(() => provider.trackExecutionSuccess()).not.toThrow()
|
||||
expect(() => provider.trackExecutionError()).not.toThrow()
|
||||
expect(() => provider.trackPageView('ignored')).not.toThrow()
|
||||
expect(() =>
|
||||
provider.trackExecutionStarted({ jobId: 'job-a', startTime: 1 })
|
||||
).not.toThrow()
|
||||
expect(() =>
|
||||
provider.trackExecutionSuccess({ jobId: 'job-a' })
|
||||
).not.toThrow()
|
||||
expect(() => provider.trackExecutionError({ jobId: 'job-a' })).not.toThrow()
|
||||
expect(addAction).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import type { PageViewMetadata, TelemetryProvider } from '../../types'
|
||||
import type {
|
||||
ExecutionErrorMetadata,
|
||||
ExecutionStartMetadata,
|
||||
ExecutionSuccessMetadata,
|
||||
PageViewMetadata,
|
||||
TelemetryProvider
|
||||
} from '../../types'
|
||||
|
||||
interface DatadogRumInternalContext {
|
||||
view?: { id?: string }
|
||||
}
|
||||
|
||||
interface DatadogRumClient {
|
||||
addAction(name: string, context?: Record<string, unknown>): void
|
||||
setViewContextProperty(name: string, value: unknown): void
|
||||
getInternalContext(startTime?: number): DatadogRumInternalContext | undefined
|
||||
setViewName(name: string): void
|
||||
}
|
||||
|
||||
interface WindowWithDatadogRum extends Window {
|
||||
@@ -10,11 +21,10 @@ interface WindowWithDatadogRum extends Window {
|
||||
}
|
||||
|
||||
const WORKFLOW_CONTEXT = {
|
||||
product: 'cloud_generation',
|
||||
product_surface: 'workspace'
|
||||
product: 'cloud_generation'
|
||||
} as const
|
||||
|
||||
type ProductSurface =
|
||||
type ViewName =
|
||||
| 'account_access'
|
||||
| 'oauth_consent'
|
||||
| 'support_recovery'
|
||||
@@ -30,14 +40,7 @@ function getDatadogRum(): DatadogRumClient | undefined {
|
||||
return (window as WindowWithDatadogRum).DD_RUM
|
||||
}
|
||||
|
||||
function getNavigationType(): NavigationTimingType | 'unknown' {
|
||||
const [navigation] = performance.getEntriesByType(
|
||||
'navigation'
|
||||
) as PerformanceNavigationTiming[]
|
||||
return navigation?.type ?? 'unknown'
|
||||
}
|
||||
|
||||
function getProductSurface(path = window.location.href): ProductSurface {
|
||||
function getViewName(path = window.location.href): ViewName {
|
||||
const pathname = new URL(path, window.location.origin).pathname.replace(
|
||||
/\/$/,
|
||||
''
|
||||
@@ -50,36 +53,43 @@ function getProductSurface(path = window.location.href): ProductSurface {
|
||||
}
|
||||
|
||||
export class DatadogRumTelemetryProvider implements TelemetryProvider {
|
||||
constructor() {
|
||||
getDatadogRum()?.setViewContextProperty(
|
||||
'navigation_type',
|
||||
getNavigationType()
|
||||
)
|
||||
}
|
||||
private readonly originViewIdsByJobId = new Map<string, string>()
|
||||
|
||||
trackPageView(_pageName: string, properties?: PageViewMetadata): void {
|
||||
getDatadogRum()?.setViewContextProperty(
|
||||
'product_surface',
|
||||
getProductSurface(properties?.path)
|
||||
)
|
||||
getDatadogRum()?.setViewName(getViewName(properties?.path))
|
||||
}
|
||||
|
||||
trackWorkflowExecution(): void {
|
||||
getDatadogRum()?.addAction('workflow_execution_started', WORKFLOW_CONTEXT)
|
||||
trackExecutionStarted(metadata: ExecutionStartMetadata): void {
|
||||
const rum = getDatadogRum()
|
||||
const originViewId = rum?.getInternalContext(metadata.startTime)?.view?.id
|
||||
if (originViewId)
|
||||
this.originViewIdsByJobId.set(metadata.jobId, originViewId)
|
||||
|
||||
rum?.addAction('workflow_execution_started', {
|
||||
...WORKFLOW_CONTEXT,
|
||||
...(originViewId && { origin_view_id: originViewId })
|
||||
})
|
||||
}
|
||||
|
||||
trackExecutionSuccess(): void {
|
||||
this.trackTerminalOutcome('success')
|
||||
trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void {
|
||||
this.trackTerminalOutcome(metadata.jobId, 'success')
|
||||
}
|
||||
|
||||
trackExecutionError(): void {
|
||||
this.trackTerminalOutcome('failure')
|
||||
trackExecutionError(metadata: ExecutionErrorMetadata): void {
|
||||
this.trackTerminalOutcome(metadata.jobId, 'failure')
|
||||
}
|
||||
|
||||
private trackTerminalOutcome(outcome: 'success' | 'failure'): void {
|
||||
private trackTerminalOutcome(
|
||||
jobId: string,
|
||||
outcome: 'success' | 'failure'
|
||||
): void {
|
||||
const originViewId = this.originViewIdsByJobId.get(jobId)
|
||||
this.originViewIdsByJobId.delete(jobId)
|
||||
|
||||
getDatadogRum()?.addAction('workflow_execution_completed', {
|
||||
...WORKFLOW_CONTEXT,
|
||||
outcome
|
||||
outcome,
|
||||
...(originViewId && { origin_view_id: originViewId })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +156,11 @@ export interface ExecutionErrorMetadata {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface ExecutionStartMetadata {
|
||||
jobId: string
|
||||
startTime: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Execution success metadata
|
||||
*/
|
||||
@@ -632,6 +637,7 @@ export interface TelemetryProvider {
|
||||
|
||||
// Workflow execution events
|
||||
trackWorkflowExecution?(): void
|
||||
trackExecutionStarted?(metadata: ExecutionStartMetadata): void
|
||||
trackExecutionError?(metadata: ExecutionErrorMetadata): void
|
||||
trackExecutionSuccess?(metadata: ExecutionSuccessMetadata): void
|
||||
trackSharedWorkflowRun?(metadata: SharedWorkflowRunMetadata): void
|
||||
|
||||
@@ -20,6 +20,7 @@ const {
|
||||
mockActiveWorkflow,
|
||||
mockOpenWorkflows,
|
||||
mockShowTextPreview,
|
||||
mockTrackExecutionStarted,
|
||||
mockTrackExecutionError,
|
||||
mockTrackExecutionSuccess,
|
||||
mockTrackSharedWorkflowRun
|
||||
@@ -32,6 +33,7 @@ const {
|
||||
mockActiveWorkflow: shallowRef<{ path?: string } | null>(null),
|
||||
mockOpenWorkflows: shallowRef<{ path: string }[]>([]),
|
||||
mockShowTextPreview: vi.fn(),
|
||||
mockTrackExecutionStarted: vi.fn(),
|
||||
mockTrackExecutionError: vi.fn(),
|
||||
mockTrackExecutionSuccess: vi.fn(),
|
||||
mockTrackSharedWorkflowRun: vi.fn()
|
||||
@@ -91,6 +93,7 @@ vi.mock('@/platform/distribution/types', async () => ({
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: () => ({
|
||||
trackExecutionStarted: mockTrackExecutionStarted,
|
||||
trackExecutionError: mockTrackExecutionError,
|
||||
trackExecutionSuccess: mockTrackExecutionSuccess,
|
||||
trackSharedWorkflowRun: mockTrackSharedWorkflowRun
|
||||
@@ -631,6 +634,10 @@ describe('useExecutionStore - workflowStatus', () => {
|
||||
callStoreJob('job-1', workflowA)
|
||||
fireExecutionStart('job-1')
|
||||
|
||||
expect(mockTrackExecutionStarted).toHaveBeenCalledWith({
|
||||
jobId: 'job-1',
|
||||
startTime: expect.any(Number)
|
||||
})
|
||||
expect(store.getWorkflowStatus(workflowA)).toBe('running')
|
||||
})
|
||||
|
||||
|
||||
@@ -748,6 +748,10 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
if (workflow?.path) {
|
||||
ensureSessionWorkflowPath(id, workflow.path)
|
||||
}
|
||||
useTelemetry()?.trackExecutionStarted({
|
||||
jobId: id,
|
||||
startTime: performance.now()
|
||||
})
|
||||
flushPendingWorkflowStatus(String(id), workflow)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user