@zendesk/react-measure-timing-hooks
Version:
react hooks for measuring time to interactive and time to render of components
471 lines (431 loc) • 13.5 kB
text/typescript
import { assertType, describe, expect, it } from 'vitest'
import { generateUseBeacon } from './hooks'
import type { GetRelationSchemasTFromTraceManager } from './hooksTypes'
import * as match from './matchSpan'
import { TraceManager } from './TraceManager'
import type { MapSchemaToTypes } from './types'
const mockSpanWithoutRelation = {
name: 'some-span',
duration: 0,
type: 'mark',
attributes: {},
startTime: { now: 0, epoch: 0 },
} as const
describe('type tests', () => {
const traceManager = new TraceManager({
relationSchemas: {
global: {},
ticket: { ticketId: String },
user: { userId: String },
tickedField: { ticketId: String, customFieldId: String },
custom: { customId: String, customOtherId: String },
ticketEvent: { ticketId: String, eventId: String },
},
generateId: () => 'id',
reportFn: (trace) => {
if (!trace.relatedTo) return
if ('ticketId' in trace.relatedTo) {
// valid
expect(trace.relatedTo.ticketId).toBeDefined()
// @ts-expect-error invalid relatedTo
expect(trace.relatedTo.userId).toBeDefined()
}
if ('eventId' in trace.relatedTo) {
// valid
expect(trace.relatedTo.eventId).toBeDefined()
expect(trace.relatedTo.ticketId).toBeDefined()
// @ts-expect-error invalid relatedTo
expect(trace.relatedTo.userId).toBeDefined()
}
if ('userId' in trace.relatedTo) {
// valid
expect(trace.relatedTo.userId).toBeDefined()
// @ts-expect-error invalid relatedTo
expect(trace.relatedTo.ticketId).toBeDefined()
}
// valid
if ('customFieldId' in trace.relatedTo) {
expect(trace.relatedTo.customFieldId).toBeDefined()
}
},
reportErrorFn: (error) => {
console.error(error)
},
})
interface RequiredBeaconAttributes {
team: string
}
const useBeacon = generateUseBeacon(traceManager)
type Schema = GetRelationSchemasTFromTraceManager<typeof traceManager>
const useBeaconWithRequiredAttributes = generateUseBeacon<
Schema,
RequiredBeaconAttributes
>(traceManager)
it('works', () => {
// invalid:
const invalidTraceManager = new TraceManager({
generateId: () => 'id',
reportFn: () => {},
reportErrorFn: () => {},
relationSchemas: {
// @ts-expect-error because in the matcher functions, we cannot compare objects (due to object equality comparison)
something: { blah: { test: String } },
},
})
// valid beacon
useBeacon({
name: 'OmniLog',
renderedOutput: 'content',
relatedTo: { ticketId: '123', customFieldId: '123' },
})
// valid beacon
useBeacon({
name: 'UserPage',
renderedOutput: 'content',
relatedTo: { userId: '123' },
})
// invalid
useBeacon({
name: 'UserPage',
renderedOutput: 'content',
// @ts-expect-error invalid relatedTo
relatedTo: { invalid: '123' },
})
// valid beacon
useBeacon({
name: 'OmniLog',
renderedOutput: 'content',
// @ts-expect-error invalid: missing ticketId
relatedTo: { customFieldId: '123' },
})
// valid beacon with only required attributes
useBeaconWithRequiredAttributes({
name: 'UserPage',
renderedOutput: 'content',
relatedTo: { userId: '123' },
attributes: { team: 'test' },
})
// valid beacon required attributes and additional attributes
useBeaconWithRequiredAttributes({
name: 'UserPage',
renderedOutput: 'content',
relatedTo: { userId: '123' },
attributes: { randoKey: 'test', team: 'test' },
})
// invalid beacon missing required attributes
useBeaconWithRequiredAttributes({
name: 'UserPage',
renderedOutput: 'content',
relatedTo: { userId: '123' },
// @ts-expect-error attributes require a team key
attributes: { randoKey: 'test' },
})
// valid definition
const ticketActivationTracer = traceManager.createTracer({
name: 'ticket.activated',
relationSchemaName: 'ticket',
variants: {
origin: { timeout: 5_000 },
another_origin: { timeout: 10_000 },
},
requiredSpans: [{ matchingRelations: ['ticketId'] }],
})
const ticketActivationTracer2 = traceManager.createTracer({
name: 'ticket.activated',
relationSchemaName: 'custom',
variants: {
origin: { timeout: 5_000 },
},
requiredSpans: [
match.withAllConditions(
match.withName(
(name, relations) => name === `${relations?.customId}.end`,
),
match.withName('end'),
match.withMatchingRelations(['customId']),
),
match.withName(
(name, relatedTo) => name === `${relatedTo?.customId}.end`,
),
match.withName('customFieldId'),
match.withMatchingRelations(['customId']),
// @ts-expect-error invalid relatedTo
match.withMatchingRelations(['typoId']),
],
})
// valid definition
const userPageTracer = traceManager.createTracer({
name: 'user.activation',
relationSchemaName: 'user',
variants: {
origin: { timeout: 5_000 },
},
requiredSpans: [{ matchingRelations: ['userId'] }],
})
// valid definition
const customFieldDropdownTracer = traceManager.createTracer({
name: 'ticket.custom_field',
relationSchemaName: 'tickedField',
variants: {
origin: { timeout: 5_000 },
},
requiredSpans: [{ matchingRelations: ['ticketId'] }],
})
// invalid definition. relatedTo match but not included in AllPossibleScopes
const invalidTracer = traceManager.createTracer({
name: 'ticket.activated',
variants: {
origin: { timeout: 5_000 },
},
// @ts-expect-error invalid relatedTo
relationSchemaName: ['invalid'],
requiredSpans: [
{
// @ts-expect-error invalid relatedTo
matchingRelations: ['invalid'],
},
],
})
// invalid definition. userId given in requiredSpans isn't one of the relatedTo the tracer says it can have
const shouldErrorTrace = traceManager.createTracer({
name: 'ticket.should_error',
relationSchemaName: 'tickedField',
variants: {
origin: { timeout: 5_000 },
},
requiredSpans: [
{
// @ts-expect-error invalid relatedTo
matchingRelations: ['userId'],
},
],
})
// valid definition
const ticketActivationWithFnTracer = traceManager.createTracer({
name: 'ticket.activated',
relationSchemaName: 'ticket',
variants: {
origin: { timeout: 5_000 },
},
requiredSpans: [
{ matchingRelations: ['ticketId'] },
({ span }) => span.relatedTo?.ticketId === '123',
],
})
// valid start
ticketActivationTracer.start({
relatedTo: { ticketId: '123' },
variant: 'origin',
})
// valid start
ticketActivationTracer.start({
relatedTo: { ticketId: '999' },
variant: 'another_origin',
})
// invalid start - wrong variant
ticketActivationTracer.start({
relatedTo: { ticketId: '123' },
// @ts-expect-error invalid variant
variant: 'origin_wrong',
})
// invalid start (errors)
ticketActivationTracer.start({
// @ts-expect-error invalid relatedTo
relatedTo: { whatever: '123' },
})
// invalid start (errors)
ticketActivationTracer.start({
// @ts-expect-error invalid relatedTo
relatedTo: { userId: '123' },
variant: 'origin',
})
// valid - excess relatedTo
traceManager.processSpan({
...mockSpanWithoutRelation,
relatedTo: { ticketId: '123', customFieldId: '123', userId: '123' },
})
// valid
traceManager.processSpan({
...mockSpanWithoutRelation,
relatedTo: { ticketId: '123' },
})
// valid - multiple relatedTo simultaneously
traceManager.processSpan({
...mockSpanWithoutRelation,
relatedTo: {
ticketId: '123',
customFieldId: '123',
},
})
// invalid
traceManager.processSpan({
...mockSpanWithoutRelation,
relatedTo: {
// @ts-expect-error bad relatedTo
bad: '123',
},
})
// invalid
traceManager.processSpan({
...mockSpanWithoutRelation,
relatedTo: {
// @ts-expect-error bad relatedTo
ticketId: 123,
},
})
ticketActivationTracer.addRequirementsToCurrentTraceOnly({
additionalRequiredSpans: [
{ name: 'end', matchingRelations: ['ticketId'] },
],
})
})
it('does not allow to include invalid relatedTo value', () => {
const tracer = traceManager.createTracer({
name: 'ticket.relatedTo-operation',
type: 'operation',
relationSchemaName: 'ticket',
variants: {
origin: { timeout: 5_000 },
},
requiredSpans: [{ name: 'end', matchingRelations: true }],
})
const traceId = tracer.start({
relatedTo: {
// @ts-expect-error number should not be assignable to string
ticketId: 4,
},
variant: 'origin',
})
assertType(traceId)
})
it('mixed relatedTo', () => {
const tracer = traceManager.createTracer({
name: 'ticket.relatedTo-operation',
type: 'operation',
relationSchemaName: 'tickedField',
requiredSpans: [{ name: 'end', matchingRelations: true }],
variants: { default: { timeout: 5_000 } },
})
const traceId = tracer.start({
variant: 'default',
relatedTo: {
customFieldId: '3',
ticketId: '4',
},
})
})
it('redaction example', () => {
const tracer = traceManager.createTracer({
name: 'ticket.event.redacted',
type: 'operation',
relationSchemaName: 'ticketEvent',
variants: {
origin: { timeout: 5_000 },
},
requiredSpans: [{ name: 'OmniLogEvent', matchingRelations: true }],
debounceOnSpans: [{ name: 'OmniLog', matchingRelations: ['ticketId'] }],
})
const traceId = tracer.start({
relatedTo: {
ticketId: '4',
eventId: '3',
},
variant: 'origin',
})
assertType<string | undefined>(traceId)
})
it('redaction invalid example', () => {
const tracer = traceManager.createTracer({
name: 'ticket.event.redacted',
type: 'operation',
// @ts-expect-error enforce a complete set of keys of a given relatedTo
relationSchemaName: ['eventId'],
timeout: 5_000,
requiredSpans: [{ name: 'OmniLogEvent', matchingRelations: true }],
})
const correctTracer = traceManager.createTracer({
name: 'ticket.event.redacted',
type: 'operation',
relationSchemaName: 'ticketEvent',
variants: {
origin: { timeout: 5_000 },
},
requiredSpans: [{ name: 'OmniLogEvent', matchingRelations: true }],
})
const traceId = correctTracer.start({
relatedTo: {
ticketId: '4',
// @ts-expect-error trying to start trace with invalid relatedTo combination
customFieldId: 'werwer',
},
variant: 'origin',
})
})
it('does not allow to include invalid relatedTo key', () => {
const tracer = traceManager.createTracer({
name: 'ticket.relatedTo-operation',
type: 'operation',
relationSchemaName: 'ticket',
variants: {
origin: { timeout: 5_000 },
},
requiredSpans: [{ name: 'end', matchingRelations: true }],
})
const traceId = tracer.start({
variant: 'origin',
relatedTo: {
// @ts-expect-error invalid relatedTo key
userId: '3',
},
})
assertType(traceId)
})
it('maps schema to types', () => {
const testSchema = {
a: String,
b: Number,
c: Boolean,
d: ['union', 'of', 'things', 2],
} as const
type MappedTest = MapSchemaToTypes<typeof testSchema>
assertType<{
readonly a: string
readonly b: number
readonly c: boolean
readonly d: 'union' | 'of' | 'things' | 2
}>({} as MappedTest)
})
it('maps computedValueDefinitions', () => {
const tracer = traceManager.createTracer({
name: 'ticket.multiple-computed-values',
type: 'operation',
relationSchemaName: 'global',
requiredSpans: [{ name: 'end' }],
variants: {
cold_boot: { timeout: 10_000 },
},
computedValueDefinitions: {
'valid-feature-count': {
matches: [{ name: 'feature' }, { name: 'feature-2' }],
computeValueFromMatches: (feature, feature2) =>
feature.length + feature2.length,
},
'invalid-feature-count': {
matches: [{ name: 'feature' }, { name: 'feature-2' }],
// @ts-expect-error invalid number of arguments
computeValueFromMatches: (feature, feature2, invalid) => 0,
},
'error-count': {
matches: [{ name: (name) => name.startsWith('error') }],
computeValueFromMatches: (errors) => errors.length,
},
another: {
matches: [(name) => name.span.name.startsWith('error')],
computeValueFromMatches: (errors) => errors.length,
},
// TODO: adding a function breaks the type for some odd reason
// https://github.com/microsoft/TypeScript/issues/61228
},
})
})
})