Skip to content

Commit

Permalink
chore: various small implementation improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
niieani committed Nov 23, 2024
1 parent 1f5d223 commit ce05c42
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 107 deletions.
33 changes: 23 additions & 10 deletions src/v3/matchSpan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ export type SpanMatcherFn<ScopeT extends ScopeBase> = ((
) => boolean) &
SpanMatcherTags

type NameMatcher<ScopeT extends ScopeBase> =
| string
| RegExp
| ((name: string, scope: ScopeT) => boolean)

export interface SpanMatchDefinition<ScopeT extends ScopeBase> {
// IMPLEMENTATION TODO: take in scope as a second parameter
name?: string | RegExp | ((name: string) => boolean)
// IMPLEMENTATION TODO: take in scope as a second parameter
performanceEntryName?: string | RegExp | ((name: string) => boolean)
name?: NameMatcher<ScopeT>
performanceEntryName?: NameMatcher<ScopeT>
type?: SpanType
status?: SpanStatus
attributes?: Attributes
Expand All @@ -37,27 +40,27 @@ export type SpanMatch<ScopeT extends ScopeBase> =
* The common name of the span to match. Can be a string, RegExp, or function.
*/
export function withName<ScopeT extends ScopeBase>(
value: string | RegExp | ((name: string) => boolean),
value: NameMatcher<ScopeT>,
): SpanMatcherFn<ScopeT> {
return ({ span }) => {
return ({ span }, scope) => {
if (typeof value === 'string') return span.name === value
if (value instanceof RegExp) return value.test(span.name)
return value(span.name)
return value(span.name, scope)
}
}

/**
* The PerformanceEntry.name of the entry to match. Can be a string, RegExp, or function.
*/
export function withPerformanceEntryName<ScopeT extends ScopeBase>(
value: string | RegExp | ((name: string) => boolean),
value: NameMatcher<ScopeT>,
): SpanMatcherFn<ScopeT> {
return ({ span }) => {
return ({ span }, scope) => {
const entryName = span.performanceEntry?.name
if (!entryName) return false
if (typeof value === 'string') return entryName === value
if (value instanceof RegExp) return value.test(entryName)
return value(entryName)
return value(entryName, scope)
}
}

Expand Down Expand Up @@ -111,6 +114,16 @@ export function withOccurrence<ScopeT extends ScopeBase>(
}
}

export function withComponentRenderCount<ScopeT extends ScopeBase>(
name: string,
renderCount: number,
): SpanMatcherFn<ScopeT> {
return ({ span }) => {
if (!('renderCount' in span)) return false
return span.name === name && span.renderCount === renderCount
}
}

/**
* only applicable for component-lifecycle entries
*/
Expand Down
35 changes: 17 additions & 18 deletions src/v3/observePerformanceWithTraceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,35 @@ import type { NativePerformanceEntryType } from './spanTypes'
import type { TraceManager } from './traceManager'
import type { ScopeBase } from './types'

export const getBestSupportedEntryTypes = (): NativePerformanceEntryType[] =>
(PerformanceObserver.supportedEntryTypes.includes('long-animation-frame')
? PerformanceObserver.supportedEntryTypes.filter(
(entryType) => entryType !== 'longtask',
)
: PerformanceObserver.supportedEntryTypes) as NativePerformanceEntryType[]

/**
* The function creates an instance of PerformanceObserver that integrates with a TraceManager to automatically observe
* and map specified types of performance entries from the browser's Performance API to trace entries.
* @param traceManager - An instance of TraceManager
* that will handle the processing
* of mapped performance entries.
* @param entryTypes - An array of strings specifying the types of
* performance entries to observe (e.g.,
* ["resource", "navigation"]).
*
* @template ScopeT - A generic type that extends ScopeBase, allowing for
* flexible scope definitions in the TraceManager.
*
* @param {TraceManager<ScopeT>} traceManager - An instance of TraceManager
* that will handle the processing
* of mapped performance entries.
* @param {string[]} entryTypes - An array of strings specifying the types of
* performance entries to observe (e.g.,
* ["resource", "navigation"]).
*
* @returns {() => void} A cleanup function that, when called, disconnects the
* PerformanceObserver, stopping the observation of
* performance entries.
* @returns A cleanup function that, when called, disconnects the
* PerformanceObserver, stopping the observation of
* performance entries.
*/

export const observePerformanceWithTraceManager = <ScopeT extends ScopeBase>(
traceManager: TraceManager<ScopeT>,
entryTypes: NativePerformanceEntryType[],
entryTypes = getBestSupportedEntryTypes(),
) => {
const observer = new PerformanceObserver((entryList) => {
entryList.getEntries().forEach((entry) => {
const traceEntry = getSpanFromPerformanceEntry<ScopeT>(entry)
if (traceEntry !== undefined) {
// if (entry.entryType === 'longtask') {
// console.log('Long task detected:', traceEntry)
// }
traceManager.processSpan(traceEntry)
}
})
Expand Down
34 changes: 17 additions & 17 deletions src/v3/recordingComputeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,15 @@ export function getComputedSpans<ScopeT extends ScopeBase>({
startSpanMatcher === 'operation-start'
? input.startTime.now
: recordedItems.find((spanAndAnnotation) =>
startSpanMatcher(spanAndAnnotation, input.scope),
)?.span.startTime.now
startSpanMatcher(spanAndAnnotation, input.scope),
)?.span.startTime.now

const endSpanMatcher =
endSpan === 'operation-end'
? markedComplete
: endSpan === 'interactive'
? markedInteractive
: endSpan
? markedInteractive
: endSpan

const matchingEndEntry = recordedItems.find((spanAndAnnotation) =>
endSpanMatcher(spanAndAnnotation, input.scope),
Expand Down Expand Up @@ -119,10 +119,11 @@ export function getSpanSummaryAttributes<ScopeT extends ScopeBase>({
for (const { span } of recordedItems) {
const { attributes, name } = span
const existingAttributes = spanAttributes[name] ?? {}
// IMPLEMENTATION TODO: add some basic span summarization, like count, total duration, etc.
spanAttributes[name] = {
...existingAttributes,
...attributes,
if (attributes && Object.keys(attributes).length > 0) {
spanAttributes[name] = {
...existingAttributes,
...attributes,
}
}
}

Expand Down Expand Up @@ -151,7 +152,8 @@ export function createTraceRecording<ScopeT extends ScopeBase>(
const { id, scope } = input
const { name } = definition
// TODO: let's get this information from up top (in FinalState)
const wasInterrupted = interruptionReason && transitionFromState !== 'waiting-for-interactive';
const wasInterrupted =
interruptionReason && transitionFromState !== 'waiting-for-interactive'
// TODO: maybe we don't compute spans and values when interrupted
const computedSpans = getComputedSpans(data)
const computedValues = getComputedValues(data)
Expand All @@ -161,23 +163,21 @@ export function createTraceRecording<ScopeT extends ScopeBase>(
const anyErrors = recordedItems.some(({ span }) => span.status === 'error')
const duration =
lastRequiredSpanAndAnnotation?.annotation.operationRelativeEndTime ?? null
const startTillInteractive = cpuIdleSpanAndAnnotation?.annotation.operationRelativeEndTime ?? null
const startTillInteractive =
cpuIdleSpanAndAnnotation?.annotation.operationRelativeEndTime ?? null
return {
id,
name,
startTime: input.startTime,
scope,
type: 'operation',
duration,
startTillInteractive,
// last entry until the tti?
completeTillInteractive: startTillInteractive && duration ? startTillInteractive - duration : null,
completeTillInteractive:
startTillInteractive && duration ? startTillInteractive - duration : null,
// ?: If we have any error entries then should we mark the status as 'error'
status:
wasInterrupted
? 'interrupted'
: anyErrors
? 'error'
: 'ok',
status: wasInterrupted ? 'interrupted' : anyErrors ? 'error' : 'ok',
computedSpans,
computedValues,
attributes,
Expand Down
9 changes: 5 additions & 4 deletions src/v3/spanTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface StartTraceConfig<ScopeT extends ScopeBase> {
scope: ScopeT
startTime?: Partial<Timestamp>
/**
* any attributes that should be
* any attributes that are relevant to the entire trace
*/
attributes?: Attributes
}
Expand Down Expand Up @@ -95,7 +95,7 @@ export interface SpanBase<ScopeT extends ScopeBase> {

export interface ComponentRenderSpan<ScopeT extends ScopeBase>
extends Omit<SpanBase<ScopeT>, 'scope' | 'attributes'>,
BeaconConfig<ScopeT> {
BeaconConfig<ScopeT> {
type: ComponentLifecycleSpanType
errorInfo?: ErrorInfo
renderCount: number
Expand Down Expand Up @@ -127,15 +127,16 @@ export type InitiatorType =

export interface ResourceSpan<ScopeT extends ScopeBase>
extends SpanBase<ScopeT> {
type: 'resource',
type: 'resource'
resourceDetails: {
initiatorType: InitiatorType
query: Record<string, string | string[]>
hash: string
}
}

export interface PerformanceEntrySpan<ScopeT extends ScopeBase> extends SpanBase<ScopeT> {
export interface PerformanceEntrySpan<ScopeT extends ScopeBase>
extends SpanBase<ScopeT> {
type: Exclude<NativePerformanceEntryType, 'resource'>
}

Expand Down
56 changes: 34 additions & 22 deletions src/v3/traceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import { ensureTimestamp } from './ensureTimestamp'
import { type SpanMatcherFn, fromDefinition } from './matchSpan'
import type { SpanAnnotationRecord } from './spanAnnotationTypes'
import type { Span, StartTraceConfig } from './spanTypes'
import type { TraceRecording } from './traceRecordingTypes'
import type {
CompleteTraceDefinition,
ComputedSpanDefinition,
ComputedValueDefinition,
TraceRecording,
} from './traceRecordingTypes'
import type {
CompleteTraceDefinition,
ReportFn,
ScopeBase,
TraceDefinition,
Expand Down Expand Up @@ -37,24 +35,38 @@ export class TraceManager<ScopeT extends ScopeBase> {
ScopeT,
SpanMatcherFn<ScopeT>[]
>[] = []
const requiredToEnd = traceDefinition.requiredToEnd.map(
(matcherFnOrDefinition) =>
typeof matcherFnOrDefinition === 'function'
? matcherFnOrDefinition
: fromDefinition(matcherFnOrDefinition),
) as ArrayWithAtLeastOneElement<SpanMatcherFn<ScopeT>>
const debounceOn = traceDefinition.debounceOn?.map(
(matcherFnOrDefinition) =>
typeof matcherFnOrDefinition === 'function'
? matcherFnOrDefinition
: fromDefinition(matcherFnOrDefinition),
) as ArrayWithAtLeastOneElement<SpanMatcherFn<ScopeT>> | undefined
const interruptOn = traceDefinition.interruptOn?.map(
(matcherFnOrDefinition) =>
typeof matcherFnOrDefinition === 'function'
? matcherFnOrDefinition
: fromDefinition(matcherFnOrDefinition),
) as ArrayWithAtLeastOneElement<SpanMatcherFn<ScopeT>> | undefined
const requiredToEnd =
traceDefinition.requiredToEnd && traceDefinition.requiredToEnd.length > 0
? (traceDefinition.requiredToEnd.map((matcherFnOrDefinition) =>
typeof matcherFnOrDefinition === 'function'
? matcherFnOrDefinition
: fromDefinition(matcherFnOrDefinition),
) as ArrayWithAtLeastOneElement<SpanMatcherFn<ScopeT>>)
: undefined

if (!requiredToEnd) {
throw new Error(
'requiredToEnd must be defined, as a trace will never end otherwise',
)
}

const debounceOn =
traceDefinition.debounceOn && traceDefinition.debounceOn.length > 0
? (traceDefinition.debounceOn.map((matcherFnOrDefinition) =>
typeof matcherFnOrDefinition === 'function'
? matcherFnOrDefinition
: fromDefinition(matcherFnOrDefinition),
) as ArrayWithAtLeastOneElement<SpanMatcherFn<ScopeT>>)
: undefined

const interruptOn =
traceDefinition.interruptOn && traceDefinition.interruptOn.length > 0
? (traceDefinition.interruptOn.map((matcherFnOrDefinition) =>
typeof matcherFnOrDefinition === 'function'
? matcherFnOrDefinition
: fromDefinition(matcherFnOrDefinition),
) as ArrayWithAtLeastOneElement<SpanMatcherFn<ScopeT>>)
: undefined

const completeTraceDefinition: CompleteTraceDefinition<ScopeT> = {
...traceDefinition,
Expand Down
33 changes: 7 additions & 26 deletions src/v3/traceRecordingTypes.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/* eslint-disable @typescript-eslint/consistent-indexed-object-style */
import type { SpanMatcherFn } from './matchSpan'
import type { SpanAndAnnotation } from './spanAnnotationTypes'
import type { Attributes } from './spanTypes'
import type {
MapTuple,
ScopeBase,
Timestamp,
TraceInterruptionReason,
TraceStatus,
TraceType,
Expand All @@ -21,6 +20,7 @@ export interface TraceRecordingBase<ScopeT extends ScopeBase> {
*/
name: string

startTime: Timestamp
scope: ScopeT

type: TraceType
Expand Down Expand Up @@ -53,6 +53,11 @@ export interface TraceRecordingBase<ScopeT extends ScopeBase> {
[valueName: string]: number | string | boolean
}

// TODO: should this get moved to convertToRum?
/**
* Merged attributes of the spans with the same type and name.
* If attributes changed, most recent ones overwrite older ones.
*/
spanAttributes: {
[typeAndName: string]: {
[attributeName: string]: unknown
Expand All @@ -64,27 +69,3 @@ export interface TraceRecording<ScopeT extends ScopeBase>
extends TraceRecordingBase<ScopeT> {
entries: SpanAndAnnotation<ScopeT>[]
}
/**
* Definition of custom spans
*/
export interface ComputedSpanDefinition<ScopeT extends ScopeBase> {
name: string
startSpan: SpanMatcherFn<ScopeT> | 'operation-start'
endSpan: SpanMatcherFn<ScopeT> | 'operation-end' | 'interactive'
}

/**
* Definition of custom values
*/

export interface ComputedValueDefinition<
ScopeT extends ScopeBase,
MatchersT extends SpanMatcherFn<ScopeT>[],
> {
name: string
matches: [...MatchersT]
computeValueFromMatches: (
// as many matches as match of type Span<ScopeT>
...matches: MapTuple<MatchersT, SpanAndAnnotation<ScopeT>[]>
) => number | string | boolean
}
3 changes: 3 additions & 0 deletions src/v3/typeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ export type MergedStateHandlerMethods<ScopeT extends ScopeBase> = {
) => StateHandlerReturnTypes<ScopeT>[K]
}
export type ArrayWithAtLeastOneElement<T> = [T, ...T[]]
export type MapTuple<KeysTuple extends readonly unknown[], MapToValue> = {
[Index in keyof KeysTuple]: MapToValue // T[KeysTuple[Index]]
}
Loading

0 comments on commit ce05c42

Please sign in to comment.