Skip to content

Commit 13ed846

Browse files
committed
refactor: extract into files
1 parent c58a983 commit 13ed846

13 files changed

+489
-485
lines changed

src/v3/ActiveTrace.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@
22
/* eslint-disable max-classes-per-file */
33
import { doesEntryMatchDefinition } from './doesEntryMatchDefinition'
44
import { ensureTimestamp } from './ensureTimestamp'
5+
import type { ActiveTraceConfig, Span } from './spanTypes'
56
import type {
6-
ActiveTraceConfig,
77
CompleteTraceDefinition,
8+
TraceRecording,
9+
} from './traceRecordingTypes'
10+
import type {
811
ScopeBase,
9-
Span,
1012
SpanAndAnnotationEntry as SpanAndAnnotation,
1113
SpanAnnotation,
1214
SpanAnnotationRecord,
1315
Timestamp,
1416
TraceInterruptionReason,
15-
TraceRecording,
1617
} from './types'
1718
import type {
1819
DistributiveOmit,

src/v3/convertToRum.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
/* eslint-disable @typescript-eslint/consistent-indexed-object-style */
2-
import {
3-
ComponentRenderSpan,
4-
ScopeBase,
5-
Span,
6-
SpanAndAnnotationEntry,
7-
TraceRecording,
8-
TraceRecordingBase,
9-
} from './types'
2+
import { ComponentRenderSpan, Span } from './spanTypes'
3+
import { TraceRecording, TraceRecordingBase } from './traceRecordingTypes'
4+
import { ScopeBase, SpanAndAnnotationEntry } from './types'
105

116
export interface EmbeddedEntry {
127
count: number

src/v3/docs/RFC.md

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -89,59 +89,7 @@ The model is heavily inspired by OpenTelemetry, adapting it to the frontend’s
8989

9090
### Model Overview
9191

92-
The primary way to capture metrics would be to record a **Product Operation trace**. Such a **trace** would include the **spans** that occurred while the application was changing state, e.g. after a user performed a specific operation. The associated trace recording would then contain browser **spans**, like network requests, or other PerformanceEntries, as well as custom **spans**, like component renders.
93-
94-
The Frontend Tracing model works as follows:
95-
96-
1. The **Tracing Engine** is a singleton instantiated when the application boots. It provides the public API for initiating a new Trace using one of the **Trace Definitions**.
97-
2. **Trace Definition files** are created once a decision to instrument observability of a given **Product Operation** is made. The definitions include the `name` of the operation, along with the criteria by which **spans** required to consider the **Product Operation** `complete` are matched, as well as other optional details (such as: criteria by which an operation may be interrupted or how long it can be active before timing-out).
98-
3. A **Trace** is started imperatively, in response to a user’s action (such as _clicking_ or _pressing a key_), or a remote event (e.g. _receiving a message, or a new notification_).
99-
Only a single **Product Operation Trace** can be active at a time.
100-
4. **Tracing Engine** listens for new **spans**, which are processed against the **active Trace’s Definition**, and stored in a list.
101-
5. The **Trace** is considered _complete_ once all of the _required_ **spans** have been seen, or
102-
6. The **Trace** is considered _interrupted,_ if any of the following occurs:
103-
1. an _interrupting_ **spans** has been seen (as per trace definition)
104-
2. the **Trace** is manually interrupted
105-
3. the **Trace** times-out
106-
4. another **Trace** starts
107-
7. The **Tracing Engine** generates a serializable **Trace Recording** containing all the seen **spans**. The resulting JSON output can be:
108-
1. sent to an observability service for analysis (we plan on using Datadog RUM)
109-
2. used to calculate individual metrics from the trace (e.g. for long-term analysis these might be stored in Datadog Metrics)
110-
3. downloaded as a file and visualized (when debugging a problem locally)
111-
112-
![tracing-engine](./tracing-engine.png)
113-
114-
### Deriving SLIs and other metrics from a trace
115-
116-
ℹ️ It is our recommendation that the primary way of creating duration metrics would be to derive them from data in the trace.
117-
118-
Instead of the traditional approach of capturing isolated metrics imperatively in the code, the **trace** model allows us the flexibility to define and compute any number of metrics from the **trace recording**.
119-
120-
We can distinguish the following types of metrics:
121-
122-
1. **Duration of a Computed Span** — the time between any two **spans** that appeared in the **trace**. For example:
123-
1. _time between the user’s click on a ticket_ and _everything in the ticket page has fully rendered with content_ (duration of the entire operation)
124-
2. _time between the user’s click on a ticket_ and _the moment the first piece of the ticket UI was displayed_ (duration of a segment of the operation)
125-
126-
![trace](./trace.png)
127-
128-
2. **Computed Values** — any numerical value derived from the **spans** or their attributes. For example:
129-
1. _The total number of times the OmniLog re-renders during a ticket load_
130-
2. _The total number of requests to agent-graph made while loading the ticket_
131-
3. _The total number of ZAF apps initialized while loading the ticket_
132-
133-
### Measuring _Visually Complete_ in React
134-
135-
Knowing whether a component is visually complete can be a challenge due to React’s renderer design, as it is impossible to deterministically tell how many times a given component will re-render in a given state. In order to ensure the Trace is as complete as possible, the **Trace Definition** may define a list of **spans** that will trigger a debouncing period, during which we may allow for these **spans** to “settle”.
136-
137-
### _Visually Complete_ vs _Fully Interactive_
138-
139-
Due to the single-threaded, event-loop-based nature of JavaScript, both asynchronous work, and DOM painting scheduled during a **Product Operation** may continue even after all the end criteria have been satisfied.
140-
While the end of a **Product Operation** gives us an indication of when the operation is _visually complete_, the user might still be blocked from interacting with the page. Thus, the duration of a **Trace** may not fully reflect the user’s _actual_ experience on the page.
141-
142-
The **Trace Definition** may be configured to continue recording additional **spans**, until the page becomes fully interactive. The _fully-interactive_ time is determined by a configurable _quiet period_, in which no significant “[long-task](https://developer.mozilla.org/en-US/docs/Glossary/Long_task)” or “[long-animation-frame](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Long_animation_frame_timing)**spans** have been seen.
143-
144-
These additional **spans** won’t contribute to the reported duration of the Trace, but may be used to create computed **spans** (e.g. Time To Interactive — TTI).
92+
[model-overview.md](./model-overview.md)
14593

14694
### Not in Scope
14795

src/v3/docs/model-overview.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
### Model Overview
2+
3+
The primary way to capture metrics would be to record a **Product Operation trace**. Such a **trace** would include the **spans** that occurred while the application was changing state, e.g. after a user performed a specific operation. The associated trace recording would then contain browser **spans**, like network requests, or other PerformanceEntries, as well as custom **spans**, like component renders.
4+
5+
The Frontend Tracing model works as follows:
6+
7+
1. The **Tracing Engine** is a singleton instantiated when the application boots. It provides the public API for initiating a new Trace using one of the **Trace Definitions**.
8+
2. **Trace Definition files** are created once a decision to instrument observability of a given **Product Operation** is made. The definitions include the `name` of the operation, along with the criteria by which **spans** required to consider the **Product Operation** `complete` are matched, as well as other optional details (such as: criteria by which an operation may be interrupted or how long it can be active before timing-out).
9+
3. A **Trace** is started imperatively, in response to a user’s action (such as _clicking_ or _pressing a key_), or a remote event (e.g. _receiving a message, or a new notification_).
10+
Only a single **Product Operation Trace** can be active at a time.
11+
4. **Tracing Engine** listens for new **spans**, which are processed against the **active Trace’s Definition**, and stored in a list.
12+
5. The **Trace** is considered _complete_ once all of the _required_ **spans** have been seen, or
13+
6. The **Trace** is considered _interrupted,_ if any of the following occurs:
14+
1. an _interrupting_ **spans** has been seen (as per trace definition)
15+
2. the **Trace** is manually interrupted
16+
3. the **Trace** times-out
17+
4. another **Trace** starts
18+
7. The **Tracing Engine** generates a serializable **Trace Recording** containing all the seen **spans**. The resulting JSON output can be:
19+
1. sent to an observability service for analysis (we plan on using Datadog RUM)
20+
2. used to calculate individual metrics from the trace (e.g. for long-term analysis these might be stored in Datadog Metrics)
21+
3. downloaded as a file and visualized (when debugging a problem locally)
22+
23+
![tracing-engine](./tracing-engine.png)
24+
25+
### Deriving SLIs and other metrics from a trace
26+
27+
ℹ️ It is our recommendation that the primary way of creating duration metrics would be to derive them from data in the trace.
28+
29+
Instead of the traditional approach of capturing isolated metrics imperatively in the code, the **trace** model allows us the flexibility to define and compute any number of metrics from the **trace recording**.
30+
31+
We can distinguish the following types of metrics:
32+
33+
1. **Duration of a Computed Span** — the time between any two **spans** that appeared in the **trace**. For example:
34+
1. _time between the user’s click on a ticket_ and _everything in the ticket page has fully rendered with content_ (duration of the entire operation)
35+
2. _time between the user’s click on a ticket_ and _the moment the first piece of the ticket UI was displayed_ (duration of a segment of the operation)
36+
37+
![trace](./trace.png)
38+
39+
2. **Computed Values** — any numerical value derived from the **spans** or their attributes. For example:
40+
1. _The total number of times the OmniLog re-renders during a ticket load_
41+
2. _The total number of requests to agent-graph made while loading the ticket_
42+
3. _The total number of ZAF apps initialized while loading the ticket_
43+
44+
### Measuring _Visually Complete_ in React
45+
46+
Knowing whether a component is visually complete can be a challenge due to React’s renderer design, as it is impossible to deterministically tell how many times a given component will re-render in a given state. In order to ensure the Trace is as complete as possible, the **Trace Definition** may define a list of **spans** that will trigger a debouncing period, during which we may allow for these **spans** to “settle”.
47+
48+
### _Visually Complete_ vs _Fully Interactive_
49+
50+
Due to the single-threaded, event-loop-based nature of JavaScript, both asynchronous work, and DOM painting scheduled during a **Product Operation** may continue even after all the end criteria have been satisfied.
51+
While the end of a **Product Operation** gives us an indication of when the operation is _visually complete_, the user might still be blocked from interacting with the page. Thus, the duration of a **Trace** may not fully reflect the user’s _actual_ experience on the page.
52+
53+
The **Trace Definition** may be configured to continue recording additional **spans**, until the page becomes fully interactive. The _fully-interactive_ time is determined by a configurable _quiet period_, in which no significant “[long-task](https://developer.mozilla.org/en-US/docs/Glossary/Long_task)” or “[long-animation-frame](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Long_animation_frame_timing)**spans** have been seen.
54+
55+
These additional **spans** won’t contribute to the reported duration of the Trace, but may be used to create computed **spans** (e.g. Time To Interactive — TTI).

src/v3/doesEntryMatchDefinition.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { doesEntryMatchDefinition } from './doesEntryMatchDefinition'
2+
import type { ScopeBase, SpanAnnotation } from './types'
23
import type {
34
ComponentRenderSpan,
4-
ScopeBase,
55
Span,
6-
SpanAnnotation,
76
SpanBase,
87
SpanMatcher,
9-
} from './types'
8+
} from './spanTypes'
109

1110
// Mock data for TraceEntryBase
1211
interface TestScopeT extends ScopeBase {

src/v3/doesEntryMatchDefinition.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { ScopeBase, SpanAndAnnotationEntry, SpanMatcher } from './types'
1+
import { ScopeBase, SpanAndAnnotationEntry } from './types'
2+
import { SpanMatcher } from './spanTypes'
23

34
/**
45
* Matches criteria against a performance entry event.
@@ -44,18 +45,18 @@ export function doesEntryMatchDefinition<ScopeT extends ScopeBase>(
4445
!attributes ||
4546
Boolean(
4647
span.attributes &&
47-
Object.entries(attributes).every(
48-
([key, value]) => span.attributes?.[key] === value,
49-
),
48+
Object.entries(attributes).every(
49+
([key, value]) => span.attributes?.[key] === value,
50+
),
5051
)
5152

5253
const matchesScope =
5354
!scope ||
5455
Boolean(
5556
span.scope &&
56-
Object.entries(scope).every(
57-
([key, value]) => span.scope?.[key] === value,
58-
),
57+
Object.entries(scope).every(
58+
([key, value]) => span.scope?.[key] === value,
59+
),
5960
)
6061

6162
const spanIsIdle = 'isIdle' in span ? span.isIdle : false

src/v3/getSpanFromPerformanceEntry.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import { getCommonUrlForTracing } from '../main'
22
import { ensureTimestamp } from './ensureTimestamp'
3-
import {
4-
Attributes,
5-
InitiatorType,
6-
ScopeBase,
7-
Span,
8-
SpanType,
9-
Timestamp,
10-
} from './types'
3+
import { ScopeBase, Timestamp } from './types'
4+
import { Attributes, InitiatorType, Span, SpanType } from './spanTypes'
115

126
/**
137
* Maps Performance Entry to Trace Entry

src/v3/hooks.ts

Lines changed: 51 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { useEffect, useRef } from 'react'
22
import { useOnComponentUnmount } from '../ErrorBoundary'
33
import { ensureTimestamp } from './ensureTimestamp'
4-
import type { TraceManager } from './traceManager'
54
import type {
65
BeaconConfig,
7-
ComponentRenderSpan,
86
GetScopeTFromTraceManager,
9-
ScopeBase,
10-
Timestamp,
117
UseBeacon,
12-
} from './types'
8+
} from './hooksTypes'
9+
import type { ComponentRenderSpan } from './spanTypes'
10+
import type { TraceManager } from './traceManager'
11+
import type { ScopeBase, Timestamp } from './types'
1312

1413
type MakeEntryInput<ScopeT extends ScopeBase> = Omit<
1514
ComponentRenderSpan<ScopeT>,
@@ -31,59 +30,59 @@ export const generateUseBeacon =
3130
<ScopeT extends ScopeBase>(
3231
traceManager: TraceManager<ScopeT>,
3332
): UseBeacon<ScopeT> =>
34-
(config: BeaconConfig<GetScopeTFromTraceManager<TraceManager<ScopeT>>>) => {
35-
const renderCountRef = useRef(0)
36-
renderCountRef.current += 1
37-
38-
// TODO: do we need to keep the render count in attributes or does this just become `occurrence` later? How did this work in the previous implementation
39-
const attributes = {
40-
...config.attributes,
41-
renderCount: renderCountRef.current,
42-
}
33+
(config: BeaconConfig<GetScopeTFromTraceManager<TraceManager<ScopeT>>>) => {
34+
const renderCountRef = useRef(0)
35+
renderCountRef.current += 1
4336

44-
const status = config.error ? 'error' : 'ok'
37+
// TODO: do we need to keep the render count in attributes or does this just become `occurrence` later? How did this work in the previous implementation
38+
const attributes = {
39+
...config.attributes,
40+
renderCount: renderCountRef.current,
41+
}
4542

46-
const renderStartTaskEntry: ComponentRenderSpan<ScopeT> = makeEntry({
47-
...config,
48-
type: 'component-render-start',
49-
duration: 0,
50-
attributes,
51-
status,
52-
})
43+
const status = config.error ? 'error' : 'ok'
5344

54-
traceManager.processSpan(renderStartTaskEntry)
45+
const renderStartTaskEntry: ComponentRenderSpan<ScopeT> = makeEntry({
46+
...config,
47+
type: 'component-render-start',
48+
duration: 0,
49+
attributes,
50+
status,
51+
})
5552

56-
// Beacon effect for tracking 'component-render'. This will fire after every render as it does not have any dependencies:
57-
useEffect(() => {
58-
traceManager.processSpan(
59-
makeEntry({
60-
...config,
61-
type: 'component-render',
62-
// TODO: the previous implementation had `operationManager.performance.now()`. Was this different?
63-
duration: performance.now() - renderStartTaskEntry.startTime.now,
64-
status,
65-
attributes,
66-
}),
67-
)
68-
})
53+
traceManager.processSpan(renderStartTaskEntry)
6954

70-
// Beacon effect for tracking 'component-unmount' entries
71-
useOnComponentUnmount(
72-
(errorBoundaryMetadata) => {
73-
const unmountEntry = makeEntry({
74-
...config,
75-
type: 'component-unmount',
76-
attributes,
77-
error: errorBoundaryMetadata?.error,
78-
errorInfo: errorBoundaryMetadata?.errorInfo,
79-
duration: 0, // TODO: is 0 duration correct?
80-
status: errorBoundaryMetadata?.error ? 'error' : 'ok',
81-
})
82-
traceManager.processSpan(unmountEntry)
83-
},
84-
[config.name],
55+
// Beacon effect for tracking 'component-render'. This will fire after every render as it does not have any dependencies:
56+
useEffect(() => {
57+
traceManager.processSpan(
58+
makeEntry({
59+
...config,
60+
type: 'component-render',
61+
// TODO: the previous implementation had `operationManager.performance.now()`. Was this different?
62+
duration: performance.now() - renderStartTaskEntry.startTime.now,
63+
status,
64+
attributes,
65+
}),
8566
)
86-
}
67+
})
68+
69+
// Beacon effect for tracking 'component-unmount' entries
70+
useOnComponentUnmount(
71+
(errorBoundaryMetadata) => {
72+
const unmountEntry = makeEntry({
73+
...config,
74+
type: 'component-unmount',
75+
attributes,
76+
error: errorBoundaryMetadata?.error,
77+
errorInfo: errorBoundaryMetadata?.errorInfo,
78+
duration: 0, // TODO: is 0 duration correct?
79+
status: errorBoundaryMetadata?.error ? 'error' : 'ok',
80+
})
81+
traceManager.processSpan(unmountEntry)
82+
},
83+
[config.name],
84+
)
85+
}
8786

8887
// Just for example and type checking
8988
// const tracingManager = new TraceManager({

src/v3/hooksTypes.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Attributes } from './spanTypes'
2+
import type { TraceManager } from './traceManager'
3+
import type { ScopeBase } from './types'
4+
5+
export type RenderedOutput = 'null' | 'loading' | 'content' | 'error'
6+
7+
export interface BeaconConfig<ScopeT extends ScopeBase> {
8+
name: string
9+
scope: ScopeT
10+
renderedOutput: RenderedOutput
11+
isIdle: boolean
12+
attributes: Attributes
13+
error?: Error
14+
}
15+
16+
export type UseBeacon<ScopeT extends ScopeBase> = (
17+
beaconConfig: BeaconConfig<ScopeT>,
18+
) => void
19+
20+
export type GetScopeTFromTraceManager<
21+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
22+
TraceManagerT extends TraceManager<any>,
23+
> = TraceManagerT extends TraceManager<infer ScopeT> ? ScopeT : never

0 commit comments

Comments
 (0)