Skip to content

Commit

Permalink
feat: allow configuring loadingStages and customizing reportFn (#6)
Browse files Browse the repository at this point in the history
also fixes the issue of "$previousStage to $stage" being both reported as the same

Co-authored-by: Bazyli Brzóska <bazyli.brzoska@gmail.com>
Co-authored-by: Cynthia Ma <cma@zendesk.com>
  • Loading branch information
3 people authored Jun 8, 2023
1 parent 2325fe0 commit 95a1c5b
Show file tree
Hide file tree
Showing 14 changed files with 418 additions and 380 deletions.
94 changes: 46 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ First, create a file that generates and exports the hooks for your metric ID pre
For example:

```typescript
import { generateTimingHooks } from '@zendesk/react-measure-timing-hooks'
import {
generateTimingHooks,
generateReport,
} from '@zendesk/react-measure-timing-hooks'

export const {
// the name of the hook(s) are generated based on the `name` and the placement names
Expand All @@ -42,7 +45,8 @@ export const {
{
name: 'Conversation',
idPrefix: 'ticket/conversation',
reportFn: (report, metadata) => {
reportFn: (reportArguments) => {
const defaultReport = generateReport(reportArguments)
// do something with the report, e.g. send select data to Datadog RUM or other analytics
},
},
Expand Down Expand Up @@ -100,8 +104,9 @@ const Conversation = ({ conversationId }) => {
```

An `idSuffix` is required when the component is not expected to be a singleton. If you're uncertain, the following heuristic should help you make the decision whether to add one:
* When the same component is rendered multiple times on a page.
* We expect the component will vary its content (re-render) based on some object variable. For example, its contents change based on another item's selection/visibility, or an action, like opening.

- When the same component is rendered multiple times on a page.
- We expect the component will vary its content (re-render) based on some object variable. For example, its contents change based on another item's selection/visibility, or an action, like opening.

### Report

Expand Down Expand Up @@ -172,9 +177,7 @@ If `isActive` is present in at least one of the beacons, timing will start from
```tsx
import { generateTimingHooks } from '@zendesk/react-measure-timing-hooks'

export const {
useSomethingLoadingTimingInMyComponent,
} = generateTimingHooks(
export const { useSomethingLoadingTimingInMyComponent } = generateTimingHooks(
{
name: 'SomethingLoading',
idPrefix: 'some/identifier',
Expand All @@ -201,9 +204,7 @@ import {
DEFAULT_STAGES,
} from '@zendesk/react-measure-timing-hooks'

export const {
useSomethingLoadingTimingInMyComponent,
} = generateTimingHooks(
export const { useSomethingLoadingTimingInMyComponent } = generateTimingHooks(
{
name: 'SomethingLoading',
idPrefix: 'some/identifier',
Expand Down Expand Up @@ -237,9 +238,7 @@ import {
switchFn,
} from '@zendesk/react-measure-timing-hooks'

export const {
useSomethingLoadingTimingInMyComponent,
} = generateTimingHooks(
export const { useSomethingLoadingTimingInMyComponent } = generateTimingHooks(
{
name: 'SomethingLoading',
idPrefix: 'some/identifier',
Expand All @@ -260,6 +259,8 @@ const MyComponent = () => {
{ case: isLoading, return: DEFAULT_STAGES.LOADING },
{ return: DEFAULT_STAGES.READY },
),
// we want generateReport to count both of these stages as part of the "loading" process
loadingStages: [DEFAULT_STAGES.LOADING, 'searching'],
})
return <div>Hello!</div>
}
Expand Down Expand Up @@ -358,19 +359,18 @@ to reset.
```tsx
import { generateTimingHooks } from '@zendesk/react-measure-timing-hooks'

export const {
useSomethingLoadingTimingInSomeComponentName,
} = generateTimingHooks(
{
name: 'SomethingLoading',
idPrefix: 'some/identifier',
finalStages: [DEFAULT_STAGES.READY],
reportFn: myCustomReportFunction,
},
// name of the first placement
// usually the component that mounts first, from which timing should start
'SomeComponentName',
)
export const { useSomethingLoadingTimingInSomeComponentName } =
generateTimingHooks(
{
name: 'SomethingLoading',
idPrefix: 'some/identifier',
finalStages: [DEFAULT_STAGES.READY],
reportFn: myCustomReportFunction,
},
// name of the first placement
// usually the component that mounts first, from which timing should start
'SomeComponentName',
)

const MyComponent = () => {
useSomethingLoadingTimingInSomeComponentName(
Expand Down Expand Up @@ -431,17 +431,16 @@ a dependency has changed (e.g. `ticketId` flipping from `-1` to the actual ID).
```tsx
import { generateTimingHooks } from '@zendesk/react-measure-timing-hooks'

export const {
useSomethingLoadingTimingInSomeComponentName,
} = generateTimingHooks(
{
name: 'SomethingLoading',
idPrefix: 'some/identifier',
finalStages: [DEFAULT_STAGES.READY],
reportFn: myCustomReportFunction,
},
'SomeComponentName',
)
export const { useSomethingLoadingTimingInSomeComponentName } =
generateTimingHooks(
{
name: 'SomethingLoading',
idPrefix: 'some/identifier',
finalStages: [DEFAULT_STAGES.READY],
reportFn: myCustomReportFunction,
},
'SomeComponentName',
)

const MyComponent = () => {
useSomethingLoadingTimingInSomeComponentName(
Expand Down Expand Up @@ -471,17 +470,16 @@ end immediately with the `lastStage: 'error'` and send out a report.
```tsx
import { generateTimingHooks } from '@zendesk/react-measure-timing-hooks'

export const {
useSomethingLoadingTimingInSomeComponentName,
} = generateTimingHooks(
{
name: 'SomethingLoading',
idPrefix: 'some/identifier',
finalStages: [DEFAULT_STAGES.READY],
reportFn: myCustomReportFunction,
},
'SomeComponentName',
)
export const { useSomethingLoadingTimingInSomeComponentName } =
generateTimingHooks(
{
name: 'SomethingLoading',
idPrefix: 'some/identifier',
finalStages: [DEFAULT_STAGES.READY],
reportFn: myCustomReportFunction,
},
'SomeComponentName',
)

const MyComponent = () => {
const { data, loading, error } = useQuery(myQuery)
Expand Down
56 changes: 29 additions & 27 deletions src/ActionLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { DependencyList } from 'react'
import {
ACTION_TYPE,
DEFAULT_DEBOUNCE_MS,
DEFAULT_LOADING_STAGES,
DEFAULT_TIMEOUT_MS,
ERROR_STAGES,
INFORMATIVE_STAGES,
Expand All @@ -18,14 +19,13 @@ import {
} from './constants'
import type { DebounceOptionsRef } from './debounce'
import { debounce, FlushReason, TimeoutReason } from './debounce'
import type { ReportFn } from './generateReport'
import { generateReport } from './generateReport'
import { performanceMark, performanceMeasure } from './performanceMark'
import type {
Action,
ActionWithStateMetadata,
DynamicActionLogOptions,
ReportWithInfo,
ReportArguments,
ReportFn,
ShouldResetOnDependencyChange,
SpanAction,
StageChangeAction,
Expand Down Expand Up @@ -62,7 +62,8 @@ export class ActionLog<CustomMetadata extends Record<string, unknown>> {
private lastStageBySource: Map<string, string> = new Map()

finalStages: readonly string[] = NO_FINAL_STAGES
immediateSendStages: readonly string[] = NO_IMMEDIATE_SEND_STAGES
loadingStages: readonly string[] = DEFAULT_LOADING_STAGES
immediateSendReportStages: readonly string[] = NO_IMMEDIATE_SEND_STAGES

private dependenciesBySource: Map<string, DependencyList> = new Map()
private hasReportedAtLeastOnce = false
Expand Down Expand Up @@ -156,7 +157,8 @@ export class ActionLog<CustomMetadata extends Record<string, unknown>> {
debounceMs,
timeoutMs,
finalStages,
immediateSendStages,
loadingStages,
immediateSendReportStages,
minimumExpectedSimultaneousBeacons,
waitForBeaconActivation,
flushUponDeactivation,
Expand All @@ -177,7 +179,9 @@ export class ActionLog<CustomMetadata extends Record<string, unknown>> {
this.debounceOptionsRef.debounceMs = debounceMs ?? DEFAULT_DEBOUNCE_MS
this.debounceOptionsRef.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS
this.finalStages = finalStages ?? NO_FINAL_STAGES
this.immediateSendStages = immediateSendStages ?? NO_IMMEDIATE_SEND_STAGES
this.loadingStages = loadingStages ?? DEFAULT_LOADING_STAGES
this.immediateSendReportStages =
immediateSendReportStages ?? NO_IMMEDIATE_SEND_STAGES
this.flushUponDeactivation = flushUponDeactivation ?? false
this.waitForBeaconActivation = waitForBeaconActivation ?? []
}
Expand Down Expand Up @@ -306,22 +310,22 @@ export class ActionLog<CustomMetadata extends Record<string, unknown>> {
info: Omit<StageChangeAction, 'type' | 'marker' | 'entry' | 'timestamp'>,
previousStage: string = INFORMATIVE_STAGES.INITIAL,
) {
const { stage } = info
const { stage, renderEntry } = info
const previousStageEntry = this.lastStageEntry
const measureName = previousStageEntry
? `${this.id}/${info.source}/${previousStage}-till-${stage}`
: `${this.id}/${info.source}/start-${stage}`

const entry = previousStageEntry
? performanceMeasure(measureName, previousStageEntry)
: performanceMark(measureName)
? performanceMeasure(measureName, previousStageEntry, renderEntry)
: performanceMark(measureName, { startTime: renderEntry?.startTime })

this.addAction({
...info,
type: ACTION_TYPE.STAGE_CHANGE,
marker: MARKER.POINT,
entry: Object.assign(entry, { startMark: previousStageEntry }),
timestamp: entry.startTime + entry.duration,
...info,
})
}

Expand Down Expand Up @@ -410,16 +414,13 @@ export class ActionLog<CustomMetadata extends Record<string, unknown>> {
new PerformanceObserver((entryList: PerformanceObserverEntryList) => {
if (!this.isCapturingData) return

const mark = performanceMark(`${this.id}/longTaskEnd`)

const entries = entryList.getEntries()

for (const entry of entries) {
this.addSpan({
type: ACTION_TYPE.UNRESPONSIVE,
source: OBSERVER_SOURCE,
// workaround for the fact that long-task performance measures do not have unique names:
entry: Object.assign(entry, { endMark: mark }),
entry,
})
}
})
Expand Down Expand Up @@ -520,7 +521,7 @@ export class ActionLog<CustomMetadata extends Record<string, unknown>> {
get isInImmediateSendStage(): boolean {
return (
ERROR_STAGES.includes(this.lastStage) ||
this.immediateSendStages.includes(this.lastStage)
this.immediateSendReportStages.includes(this.lastStage)
)
}

Expand Down Expand Up @@ -648,19 +649,21 @@ export class ActionLog<CustomMetadata extends Record<string, unknown>> {
)
}

const report = generateReport({
actions: this.actions,
timingId: this.id,
isFirstLoad: !this.hasReportedAtLeastOnce,
immediateSendStages: [...ERROR_STAGES, ...this.immediateSendStages],
})

const metadataValues = [...this.customMetadataBySource.values()]
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const metadata: CustomMetadata = Object.assign({}, ...metadataValues)

const reportWithMeta: ReportWithInfo = {
...report,
const reportArgs: ReportArguments<CustomMetadata> = {
actions: this.actions,
metadata,
loadingStages: this.loadingStages,
finalStages: this.finalStages,
immediateSendReportStages:
this.immediateSendReportStages.length > 0
? [...ERROR_STAGES, ...this.immediateSendReportStages]
: ERROR_STAGES,
timingId: this.id,
isFirstLoad: !this.hasReportedAtLeastOnce,
maximumActiveBeaconsCount:
highestNumberOfActiveBeaconsCountAtAnyGivenTime,
minimumExpectedSimultaneousBeacons:
Expand All @@ -676,13 +679,12 @@ export class ActionLog<CustomMetadata extends Record<string, unknown>> {
new Error(
`useTiming: reportFn was not set, please set it to a function that will be called with the timing report`,
),
reportWithMeta,
metadata,
reportArgs,
)
}

if (hadReachedTheRequiredActiveBeaconsCount) {
this.reportFn(report, metadata, this.actions)
this.reportFn(reportArgs)
}

// clear slate for next re-render (stop observing) and disable reporting
Expand Down
5 changes: 5 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export const DEFAULT_STAGES = {
READY: 'ready',
} as const

export const DEFAULT_LOADING_STAGES = [
DEFAULT_STAGES.LOADING,
DEFAULT_STAGES.LOADING_MORE,
] as const

export const ERROR_STAGES: readonly string[] = [
DEFAULT_STAGES.ERROR,
DEFAULT_STAGES.ERROR_BOUNDARY,
Expand Down
14 changes: 7 additions & 7 deletions src/debounce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,17 @@ export type DebouncedFn<Args extends readonly unknown[]> = ((
export const debounce = <Args extends readonly unknown[]>(
optionsRef: DebounceOptionsRef<Args>,
): DebouncedFn<Args> => {
let timeoutTimer: number | undefined
let debounceTimer: number | undefined
let timeoutTimer: ReturnType<typeof setTimeout> | undefined
let debounceTimer: ReturnType<typeof setTimeout> | undefined
let lastArgs: Args | undefined
const cancel = () => {
if (debounceTimer) window.clearTimeout(debounceTimer)
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = undefined
}
const reset = () => {
cancel()

if (timeoutTimer) window.clearTimeout(timeoutTimer)
if (timeoutTimer) clearTimeout(timeoutTimer)
timeoutTimer = undefined

const args = lastArgs
Expand Down Expand Up @@ -77,13 +77,13 @@ export const debounce = <Args extends readonly unknown[]>(
return Object.assign(
(...args: Args) => {
lastArgs = args
if (debounceTimer) window.clearTimeout(debounceTimer)
debounceTimer = window.setTimeout(
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(
() => flush(DebounceReason),
optionsRef.debounceMs,
)
if (!timeoutTimer && typeof optionsRef.timeoutMs === 'number') {
timeoutTimer = window.setTimeout(() => {
timeoutTimer = setTimeout(() => {
flush(TimeoutReason)
}, optionsRef.timeoutMs)
}
Expand Down
Loading

0 comments on commit 95a1c5b

Please sign in to comment.