UNPKG

@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
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 }, }) }) })