diff --git a/plugin-server/package.json b/plugin-server/package.json index 97ba26e2da..d6b6917aa9 100644 --- a/plugin-server/package.json +++ b/plugin-server/package.json @@ -123,6 +123,7 @@ "@types/adm-zip": "^0.4.34", "@types/babel__core": "^7.1.19", "@types/babel__standalone": "^7.1.4", + "@types/chance": "^1.1.7", "@types/faker": "^5.5.7", "@types/generic-pool": "^3.1.9", "@types/ioredis": "^4.26.4", @@ -159,6 +160,7 @@ "supertest": "^7.0.0", "ts-node": "^10.9.1", "tsc-alias": "^1.8.16", + "chance": "^1.1.13", "tsconfig-paths": "^4.2.0" }, "cyclotron": { diff --git a/plugin-server/src/cdp/templates/_destinations/native-webhook/webhook.template.test.ts b/plugin-server/src/cdp/templates/_destinations/native-webhook/webhook.template.test.ts index 8bef684918..923206a5fe 100644 --- a/plugin-server/src/cdp/templates/_destinations/native-webhook/webhook.template.test.ts +++ b/plugin-server/src/cdp/templates/_destinations/native-webhook/webhook.template.test.ts @@ -1,28 +1,111 @@ -import { template } from './webhook.template' +import { SAMPLE_GLOBALS } from '~/cdp/_tests/fixtures' + +import { NATIVE_HOG_FUNCTIONS_BY_ID } from '../../index' +import { DestinationTester, generateTestData } from '../../test/test-helpers' + +const template = NATIVE_HOG_FUNCTIONS_BY_ID['native-webhook'] + +describe(`${template.name} template`, () => { + const tester = new DestinationTester(template!) + + beforeEach(() => { + tester.beforeEach() + }) + + afterEach(() => { + tester.afterEach() + }) -describe('native webhook template', () => { it('should work with default mapping', async () => { - const mockRequest = jest.fn().mockResolvedValue({ - status: 200, - json: () => Promise.resolve({ message: 'Success' }), - text: () => Promise.resolve(JSON.stringify({ message: 'Success' })), - headers: {}, - }) - const payload = { url: 'https://example.com/webhook', method: 'POST', body: { event: '{event}', person: '{person}' }, headers: { 'Content-Type': 'application/json' }, + debug_mode: true, } + const response = await tester.invoke(SAMPLE_GLOBALS, payload) - await template.perform(mockRequest, { payload }) + expect(response.logs).toMatchInlineSnapshot(` + [ + { + "level": "debug", + "message": "config, {"method":"POST","body":{"event":{"uuid":"uuid","event":"test","distinct_id":"distinct_id","properties":{"email":"test@posthog.com"},"timestamp":"","elements_chain":"","url":""},"person":{"id":"person-id","name":"person-name","properties":{"email":"example@posthog.com"},"url":"https://us.posthog.com/projects/1/persons/1234"}},"headers":{"Content-Type":"application/json"},"debug":false,"debug_mode":true,"url":"https://example.com/webhook"}", + "timestamp": "2025-01-01T00:00:00.000Z", + }, + { + "level": "debug", + "message": "endpoint, https://example.com/webhook", + "timestamp": "2025-01-01T00:00:00.000Z", + }, + { + "level": "debug", + "message": "options, {"method":"POST","headers":{"Content-Type":"application/json"},"json":{"event":{"uuid":"uuid","event":"test","distinct_id":"distinct_id","properties":{"email":"test@posthog.com"},"timestamp":"","elements_chain":"","url":""},"person":{"id":"person-id","name":"person-name","properties":{"email":"example@posthog.com"},"url":"https://us.posthog.com/projects/1/persons/1234"}}}", + "timestamp": "2025-01-01T00:00:00.000Z", + }, + { + "level": "debug", + "message": "fetchOptions, {"method":"POST","headers":{"User-Agent":"PostHog.com/1.0","Content-Type":"application/json"},"body":"{\\"event\\":{\\"uuid\\":\\"uuid\\",\\"event\\":\\"test\\",\\"distinct_id\\":\\"distinct_id\\",\\"properties\\":{\\"email\\":\\"test@posthog.com\\"},\\"timestamp\\":\\"\\",\\"elements_chain\\":\\"\\",\\"url\\":\\"\\"},\\"person\\":{\\"id\\":\\"person-id\\",\\"name\\":\\"person-name\\",\\"properties\\":{\\"email\\":\\"example@posthog.com\\"},\\"url\\":\\"https://us.posthog.com/projects/1/persons/1234\\"}}"}", + "timestamp": "2025-01-01T00:00:00.000Z", + }, + { + "level": "debug", + "message": "convertedResponse, 200, {"status":"OK"}, {"status":"OK"}, {"content-type":"application/json"}", + "timestamp": "2025-01-01T00:00:00.000Z", + }, + { + "level": "info", + "message": "Function completed in [REPLACED]", + "timestamp": "2025-01-01T00:00:00.000Z", + }, + ] + `) + }) - expect(mockRequest).toHaveBeenCalledTimes(1) - expect(mockRequest).toHaveBeenCalledWith('https://example.com/webhook', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - json: { event: '{event}', person: '{person}' }, + it('should handle a failing request', async () => { + const inputs = generateTestData(template.id, template.inputs_schema) + + tester.mockFetchResponse({ + status: 400, + body: { status: 'ERROR' }, + headers: { 'content-type': 'application/json' }, }) + + const response = await tester.invoke(SAMPLE_GLOBALS, { ...inputs, debug_mode: true }) + + expect(response.logs).toMatchInlineSnapshot(` + [ + { + "level": "debug", + "message": "config, {"method":"POST","body":{"event":{"uuid":"uuid","event":"test","distinct_id":"distinct_id","properties":{"email":"test@posthog.com"},"timestamp":"","elements_chain":"","url":""},"person":{"id":"person-id","name":"person-name","properties":{"email":"example@posthog.com"},"url":"https://us.posthog.com/projects/1/persons/1234"}},"headers":{"Content-Type":"application/json"},"debug":false,"debug_mode":true,"url":"http://jaj.mu/iroti"}", + "timestamp": "2025-01-01T00:00:00.000Z", + }, + { + "level": "debug", + "message": "endpoint, http://jaj.mu/iroti", + "timestamp": "2025-01-01T00:00:00.000Z", + }, + { + "level": "debug", + "message": "options, {"method":"POST","headers":{"Content-Type":"application/json"},"json":{"event":{"uuid":"uuid","event":"test","distinct_id":"distinct_id","properties":{"email":"test@posthog.com"},"timestamp":"","elements_chain":"","url":""},"person":{"id":"person-id","name":"person-name","properties":{"email":"example@posthog.com"},"url":"https://us.posthog.com/projects/1/persons/1234"}}}", + "timestamp": "2025-01-01T00:00:00.000Z", + }, + { + "level": "debug", + "message": "fetchOptions, {"method":"POST","headers":{"User-Agent":"PostHog.com/1.0","Content-Type":"application/json"},"body":"{\\"event\\":{\\"uuid\\":\\"uuid\\",\\"event\\":\\"test\\",\\"distinct_id\\":\\"distinct_id\\",\\"properties\\":{\\"email\\":\\"test@posthog.com\\"},\\"timestamp\\":\\"\\",\\"elements_chain\\":\\"\\",\\"url\\":\\"\\"},\\"person\\":{\\"id\\":\\"person-id\\",\\"name\\":\\"person-name\\",\\"properties\\":{\\"email\\":\\"example@posthog.com\\"},\\"url\\":\\"https://us.posthog.com/projects/1/persons/1234\\"}}"}", + "timestamp": "2025-01-01T00:00:00.000Z", + }, + { + "level": "warn", + "message": "HTTP request failed with status 400 ({"status":"ERROR"}). ", + "timestamp": "2025-01-01T00:00:00.000Z", + }, + { + "level": "error", + "message": "Function failed: Error executing function on event uuid: Request failed with status 400 ({"status":"ERROR"})", + "timestamp": "2025-01-01T00:00:00.000Z", + }, + ] + `) }) }) diff --git a/plugin-server/src/cdp/templates/index.ts b/plugin-server/src/cdp/templates/index.ts index 7df7f2ce3c..f9add0b5e5 100644 --- a/plugin-server/src/cdp/templates/index.ts +++ b/plugin-server/src/cdp/templates/index.ts @@ -4,7 +4,7 @@ import { HogFunctionTemplate, NativeTemplate } from '../types' import { allComingSoonTemplates } from './_destinations/coming-soon/coming-soon-destinations.template' import { template as googleAdsTemplate } from './_destinations/google_ads/google.template' import { template as linearTemplate } from './_destinations/linear/linear.template' -import { template as nativeWebhook } from './_destinations/native-webhook/webhook.template' +import { template as nativeWebhookTemplate } from './_destinations/native-webhook/webhook.template' import { template as redditAdsTemplate } from './_destinations/reddit_ads/reddit.template' import { template as snapchatAdsTemplate } from './_destinations/snapchat_ads/snapchat.template' import { template as tiktokAdsTemplate } from './_destinations/tiktok_ads/tiktok.template' @@ -49,7 +49,7 @@ export const HOG_FUNCTION_TEMPLATES_TRANSFORMATIONS: HogFunctionTemplate[] = [ urlNormalizationTemplate, ] -export const NATIVE_HOG_FUNCTIONS: NativeTemplate[] = [nativeWebhook].map((plugin) => ({ +export const NATIVE_HOG_FUNCTIONS: NativeTemplate[] = [nativeWebhookTemplate].map((plugin) => ({ ...plugin, code_language: 'javascript', code: 'return event;', diff --git a/plugin-server/src/cdp/templates/test/test-helpers.ts b/plugin-server/src/cdp/templates/test/test-helpers.ts index 4b96f2cb9e..8ca4d448b9 100644 --- a/plugin-server/src/cdp/templates/test/test-helpers.ts +++ b/plugin-server/src/cdp/templates/test/test-helpers.ts @@ -1,5 +1,8 @@ +import Chance from 'chance' import merge from 'deepmerge' +import { DateTime, Settings } from 'luxon' +import { NativeDestinationExecutorService } from '~/cdp/services/native-destination-executor.service' import { defaultConfig } from '~/config/config' import { CyclotronInputType } from '~/schema/cyclotron' import { GeoIp, GeoIPService } from '~/utils/geoip' @@ -7,14 +10,17 @@ import { GeoIp, GeoIPService } from '~/utils/geoip' import { Hub } from '../../../types' import { cleanNullValues } from '../../hog-transformations/transformation-functions' import { HogExecutorService } from '../../services/hog-executor.service' +import { HogInputsService } from '../../services/hog-inputs.service' import { CyclotronJobInvocationHogFunction, CyclotronJobInvocationResult, + HogFunctionInputSchemaType, HogFunctionInvocationGlobals, HogFunctionInvocationGlobalsWithInputs, HogFunctionTemplate, HogFunctionTemplateCompiled, HogFunctionType, + NativeTemplate, } from '../../types' import { cloneInvocation } from '../../utils/invocation-utils' import { createInvocation } from '../../utils/invocation-utils' @@ -27,6 +33,87 @@ export type DeepPartialHogFunctionInvocationGlobals = { request?: HogFunctionInvocationGlobals['request'] } +const compileObject = async (obj: any): Promise => { + if (Array.isArray(obj)) { + return Promise.all(obj.map((item) => compileObject(item))) + } else if (typeof obj === 'object') { + const res: Record = {} + for (const [key, value] of Object.entries(obj)) { + res[key] = await compileObject(value) + } + return res + } else if (typeof obj === 'string') { + return await compileHog(`return f'${obj}'`) + } else { + return undefined + } +} + +const compileInputs = async ( + template: HogFunctionTemplate | NativeTemplate, + _inputs: Record +): Promise> => { + const defaultInputs = template.inputs_schema.reduce((acc, input) => { + if (typeof input.default !== 'undefined') { + acc[input.key] = input.default + } + return acc + }, {} as Record) + + const allInputs = { ...defaultInputs, ..._inputs } + + // Don't compile inputs that don't suppport templating + const compiledEntries = await Promise.all( + Object.entries(allInputs).map(async ([key, value]) => { + const schema = template.inputs_schema.find((input) => input.key === key) + if (schema?.templating === false) { + return [key, value] + } + return [key, await compileObject(value)] + }) + ) + + return compiledEntries.reduce((acc, [key, value]) => { + acc[key] = { + value: allInputs[key], + bytecode: value, + } + return acc + }, {} as Record) +} + +const createGlobals = ( + globals: DeepPartialHogFunctionInvocationGlobals = {} +): HogFunctionInvocationGlobalsWithInputs => { + return { + ...globals, + inputs: {}, + project: { id: 1, name: 'project-name', url: 'https://us.posthog.com/projects/1' }, + event: { + uuid: 'event-id', + event: 'event-name', + distinct_id: 'distinct-id', + properties: { $current_url: 'https://example.com', ...globals.event?.properties }, + timestamp: '2024-01-01T00:00:00Z', + elements_chain: '', + url: 'https://us.posthog.com/projects/1/events/1234', + ...globals.event, + }, + person: { + id: 'person-id', + name: 'person-name', + properties: { email: 'example@posthog.com', ...globals.person?.properties }, + url: 'https://us.posthog.com/projects/1/persons/1234', + ...globals.person, + }, + source: { + url: 'https://us.posthog.com/hog_functions/1234', + name: 'hog-function-name', + ...globals.source, + }, + } +} + export class TemplateTester { public template: HogFunctionTemplateCompiled private executor: HogExecutorService @@ -70,79 +157,7 @@ export class TemplateTester { } createGlobals(globals: DeepPartialHogFunctionInvocationGlobals = {}): HogFunctionInvocationGlobalsWithInputs { - return { - ...globals, - inputs: {}, - project: { id: 1, name: 'project-name', url: 'https://us.posthog.com/projects/1' }, - event: { - uuid: 'event-id', - event: 'event-name', - distinct_id: 'distinct-id', - properties: { $current_url: 'https://example.com', ...globals.event?.properties }, - timestamp: '2024-01-01T00:00:00Z', - elements_chain: '', - url: 'https://us.posthog.com/projects/1/events/1234', - ...globals.event, - }, - person: { - id: 'person-id', - name: 'person-name', - properties: { email: 'example@posthog.com', ...globals.person?.properties }, - url: 'https://us.posthog.com/projects/1/persons/1234', - ...globals.person, - }, - source: { - url: 'https://us.posthog.com/hog_functions/1234', - name: 'hog-function-name', - ...globals.source, - }, - } - } - - private async compileObject(obj: any): Promise { - if (Array.isArray(obj)) { - return Promise.all(obj.map((item) => this.compileObject(item))) - } else if (typeof obj === 'object') { - const res: Record = {} - for (const [key, value] of Object.entries(obj)) { - res[key] = await this.compileObject(value) - } - return res - } else if (typeof obj === 'string') { - return await compileHog(`return f'${obj}'`) - } else { - return undefined - } - } - - private async compileInputs(_inputs: Record): Promise> { - const defaultInputs = this.template.inputs_schema.reduce((acc, input) => { - if (typeof input.default !== 'undefined') { - acc[input.key] = input.default - } - return acc - }, {} as Record) - - const allInputs = { ...defaultInputs, ..._inputs } - - // Don't compile inputs that don't suppport templating - const compiledEntries = await Promise.all( - Object.entries(allInputs).map(async ([key, value]) => { - const schema = this.template.inputs_schema.find((input) => input.key === key) - if (schema?.templating === false) { - return [key, value] - } - return [key, await this.compileObject(value)] - }) - ) - - return compiledEntries.reduce((acc, [key, value]) => { - acc[key] = { - value: allInputs[key], - bytecode: value, - } - return acc - }, {} as Record) + return createGlobals(globals) } async invoke( @@ -153,7 +168,7 @@ export class TemplateTester { throw new Error('Mapping templates found. Use invokeMapping instead.') } - const compiledInputs = await this.compileInputs(_inputs) + const compiledInputs = await compileInputs(this.template, _inputs) const globals = this.createGlobals(_globals) const { code, ...partialTemplate } = this.template @@ -196,7 +211,7 @@ export class TemplateTester { throw new Error('No mapping templates found') } - const compiledInputs = await this.compileInputs(_inputs) + const compiledInputs = await compileInputs(this.template, _inputs) const compiledMappingInputs = { ...this.template.mapping_templates.find((mapping) => mapping.name === mapping_name), @@ -215,7 +230,7 @@ export class TemplateTester { return { key: input.key, value, - bytecode: await this.compileObject(value), + bytecode: await compileObject(value), } }) ) @@ -270,6 +285,95 @@ export class TemplateTester { } } +export class DestinationTester { + private executor: NativeDestinationExecutorService + private inputsService: HogInputsService + private mockFetch = jest.fn() + + constructor(private template: NativeTemplate) { + this.template = template + this.executor = new NativeDestinationExecutorService({} as any) + this.inputsService = new HogInputsService({} as any) + + this.executor.fetch = this.mockFetch + + this.mockFetch.mockResolvedValue({ + status: 200, + json: () => Promise.resolve({ status: 'OK' }), + text: () => Promise.resolve(JSON.stringify({ status: 'OK' })), + headers: { 'content-type': 'application/json' }, + }) + } + + createGlobals(globals: DeepPartialHogFunctionInvocationGlobals = {}): HogFunctionInvocationGlobalsWithInputs { + return createGlobals(globals) + } + + mockFetchResponse(response?: { status?: number; body?: Record; headers?: Record }) { + const defaultResponse = { + status: 200, + body: { status: 'OK' }, + headers: { 'content-type': 'application/json' }, + } + + const finalResponse = { ...defaultResponse, ...response } + + this.mockFetch.mockResolvedValue({ + status: finalResponse.status, + json: () => Promise.resolve(finalResponse.body), + text: () => Promise.resolve(JSON.stringify(finalResponse.body)), + headers: finalResponse.headers, + }) + } + + beforeEach() { + Settings.defaultZone = 'UTC' + const fixedTime = DateTime.fromISO('2025-01-01T00:00:00Z').toJSDate() + jest.spyOn(Date, 'now').mockReturnValue(fixedTime.getTime()) + } + + afterEach() { + Settings.defaultZone = 'system' + jest.useRealTimers() + } + + async invoke(globals: HogFunctionInvocationGlobals, inputs: Record) { + const compiledInputs = await compileInputs(this.template, inputs) + + const globalsWithInputs = await this.inputsService.buildInputsWithGlobals( + { + ...this.template, + inputs: compiledInputs, + } as unknown as HogFunctionType, + this.createGlobals(globals) + ) + const invocation = createInvocation(globalsWithInputs, { + ...this.template, + template_id: this.template.id, + hog: 'return event', + bytecode: [], + team_id: 1, + enabled: true, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + deleted: false, + inputs: compiledInputs, + is_addon_required: false, + }) + + const result = await this.executor.execute(invocation) + + result.logs.forEach((x) => { + if (typeof x.message === 'string' && x.message.includes('Function completed in')) { + x.message = 'Function completed in [REPLACED]' + } + }) + result.invocation.id = 'invocation-id' + + return result + } +} + export const createAdDestinationPayload = ( globals?: DeepPartialHogFunctionInvocationGlobals ): DeepPartialHogFunctionInvocationGlobals => { @@ -304,3 +408,97 @@ export const createAdDestinationPayload = ( return defaultPayload } + +export const generateTestData = ( + seedName: string, + input_schema: HogFunctionInputSchemaType[], + requiredFieldsOnly: boolean = false +): Record => { + const generateValue = (input: HogFunctionInputSchemaType): any => { + const chance = new Chance(seedName) + + if (Array.isArray(input.choices)) { + const choice = chance.pickone(input.choices) + return choice.value + } + + const getFormat = (input: HogFunctionInputSchemaType): string => { + if (input.key === 'url') { + return 'uri' + } else if (input.key === 'email') { + return 'email' + } else if (input.key === 'uuid') { + return 'uuid' + } else if (input.key === 'phone') { + return 'phone' + } + return 'string' + } + + let val: any + switch (input.type) { + case 'boolean': + val = chance.bool() + break + case 'number': + val = chance.integer() + break + default: + // covers string + switch (getFormat(input)) { + case 'date': { + const d = chance.date() + val = [d.getFullYear(), d.getMonth() + 1, d.getDate()] + .map((v) => String(v).padStart(2, '0')) + .join('-') + break + } + case 'date-time': + val = chance.date().toISOString() + break + case 'email': + val = chance.email() + break + case 'hostname': + val = chance.domain() + break + case 'ipv4': + val = chance.ip() + break + case 'ipv6': + val = chance.ipv6() + break + case 'time': { + const d = chance.date() + val = [d.getHours(), d.getMinutes(), d.getSeconds()] + .map((v) => String(v).padStart(2, '0')) + .join(':') + break + } + case 'uri': + val = chance.url() + break + case 'uuid': + val = chance.guid() + break + case 'phone': + val = chance.phone() + break + default: + val = chance.string() + break + } + break + } + return val + } + + const inputs = input_schema.reduce((acc, input) => { + if (input.required || requiredFieldsOnly === false) { + acc[input.key] = input.default ?? generateValue(input) + } + return acc + }, {} as Record) + + return inputs +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74790c6872..5b7e9742a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -161,7 +161,7 @@ importers: version: 3.12.1 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.17)(ts-node@10.9.1(@swc/core@1.10.14(@swc/helpers@0.5.15))(@types/node@22.15.17)(typescript@5.2.2)) + version: 29.7.0(@types/node@22.15.17)(ts-node@10.9.1(@swc/core@1.11.4(@swc/helpers@0.5.15))(@types/node@22.15.17)(typescript@5.2.2)) parcel: specifier: ^2.13.3 version: 2.13.3(@swc/helpers@0.5.15)(cssnano@7.0.6(postcss@8.5.6))(postcss@8.5.6)(relateurl@0.2.7)(svgo@3.3.2)(terser@5.19.1)(typescript@5.2.2) @@ -1459,6 +1459,9 @@ importers: '@types/babel__standalone': specifier: ^7.1.4 version: 7.1.4 + '@types/chance': + specifier: ^1.1.7 + version: 1.1.7 '@types/faker': specifier: ^5.5.7 version: 5.5.9 @@ -1516,6 +1519,9 @@ importers: babel-eslint: specifier: ^10.1.0 version: 10.1.0(eslint@8.57.0) + chance: + specifier: ^1.1.13 + version: 1.1.13 deepmerge: specifier: ^4.2.2 version: 4.3.1 @@ -1923,7 +1929,7 @@ importers: version: 8.57.0 jest: specifier: '*' - version: 29.7.0(@types/node@22.15.17)(ts-node@10.9.1(@swc/core@1.10.14(@swc/helpers@0.5.15))(@types/node@22.15.17)(typescript@5.2.2)) + version: 29.7.0(@types/node@22.15.17)(ts-node@10.9.1(@swc/core@1.11.4(@swc/helpers@0.5.15))(@types/node@22.15.17)(typescript@5.2.2)) kea: specifier: '*' version: 3.1.5(react@18.2.0) @@ -1977,7 +1983,7 @@ importers: version: 3.1.3 jest: specifier: '*' - version: 29.7.0(@types/node@22.15.17)(ts-node@10.9.1(@swc/core@1.10.14(@swc/helpers@0.5.15))(@types/node@22.15.17)(typescript@5.2.2)) + version: 29.7.0(@types/node@22.15.17)(ts-node@10.9.1(@swc/core@1.11.4(@swc/helpers@0.5.15))(@types/node@22.15.17)(typescript@5.2.2)) kea: specifier: '*' version: 3.1.5(react@18.2.0) @@ -7585,6 +7591,9 @@ packages: '@types/caseless@0.12.5': resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + '@types/chance@1.1.7': + resolution: {integrity: sha512-40you9610GTQPJyvjMBgmj9wiDO6qXhbfjizNYod/fmvLSfUUxURAJMTD8tjmbcZSsyYE5iEUox61AAcCjW/wQ==} + '@types/chart.js@2.9.37': resolution: {integrity: sha512-9bosRfHhkXxKYfrw94EmyDQcdjMaQPkU1fH2tDxu8DWXxf1mjzWQAV4laJF51ZbC2ycYwNDvIm1rGez8Bug0vg==} @@ -9013,6 +9022,9 @@ packages: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chance@1.1.13: + resolution: {integrity: sha512-V6lQCljcLznE7tUYUM9EOAnnKXbctE6j/rdQkYOHIWbfGQbrzTsAXNW9CdU5XCo4ArXQCj/rb6HgxPlmGJcaUg==} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -18071,7 +18083,7 @@ snapshots: '@babel/traverse': 7.28.0 '@babel/types': 7.28.1 convert-source-map: 2.0.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -18181,7 +18193,7 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-plugin-utils': 7.24.7 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -18192,7 +18204,7 @@ snapshots: '@babel/core': 7.28.0 '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-plugin-utils': 7.24.7 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -18203,7 +18215,7 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-plugin-utils': 7.24.7 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -18214,7 +18226,7 @@ snapshots: '@babel/core': 7.28.0 '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-plugin-utils': 7.24.7 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -18225,7 +18237,7 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-plugin-utils': 7.24.7 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -18236,7 +18248,7 @@ snapshots: '@babel/core': 7.28.0 '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-plugin-utils': 7.24.7 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -19675,7 +19687,7 @@ snapshots: '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/types': 7.28.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -20232,7 +20244,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) espree: 9.6.1 globals: 13.23.0 ignore: 5.2.4 @@ -20362,7 +20374,7 @@ snapshots: '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -20779,7 +20791,7 @@ snapshots: '@open-draft/until': 1.0.3 '@types/debug': 4.1.7 '@xmldom/xmldom': 0.8.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) headers-polyfill: 3.2.5 outvariant: 1.4.0 strict-event-emitter: 0.2.8 @@ -24162,7 +24174,7 @@ snapshots: '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.2.2)(webpack@5.88.2)': dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -24966,6 +24978,8 @@ snapshots: '@types/caseless@0.12.5': {} + '@types/chance@1.1.7': {} + '@types/chart.js@2.9.37': dependencies: moment: 2.29.4 @@ -25590,7 +25604,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.1.1(typescript@5.2.2) '@typescript-eslint/utils': 7.1.1(eslint@8.57.0)(typescript@5.2.2) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 ts-api-utils: 1.0.2(typescript@5.2.2) optionalDependencies: @@ -25604,7 +25618,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.1.1 '@typescript-eslint/visitor-keys': 7.1.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -25842,7 +25856,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -25850,7 +25864,7 @@ snapshots: agentkeepalive@4.3.0: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) depd: 2.0.0 humanize-ms: 1.2.1 transitivePeerDependencies: @@ -26715,6 +26729,8 @@ snapshots: chalk@5.4.1: {} + chance@1.1.13: {} + char-regex@1.0.2: {} char-regex@2.0.1: {} @@ -27815,7 +27831,7 @@ snapshots: detect-port@1.5.1: dependencies: address: 1.2.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -28193,7 +28209,7 @@ snapshots: esbuild-register@3.5.0(esbuild@0.18.20): dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) esbuild: 0.18.20 transitivePeerDependencies: - supports-color @@ -28767,7 +28783,7 @@ snapshots: dependencies: chalk: 4.1.2 commander: 5.1.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -29542,14 +29558,14 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -29562,21 +29578,21 @@ snapshots: https-proxy-agent@4.0.0: dependencies: agent-base: 5.1.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -30053,7 +30069,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) istanbul-lib-coverage: 3.2.0 source-map: 0.6.1 transitivePeerDependencies: @@ -30930,7 +30946,7 @@ snapshots: dependencies: chalk: 5.4.1 commander: 13.1.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) execa: 8.0.1 lilconfig: 3.1.3 listr2: 8.3.3 @@ -31321,7 +31337,7 @@ snapshots: micromark@2.11.4: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) parse-entities: 2.0.0 transitivePeerDependencies: - supports-color @@ -33235,7 +33251,7 @@ snapshots: puppeteer-core@2.1.1: dependencies: '@types/mime-types': 2.1.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) extract-zip: 1.7.0 https-proxy-agent: 4.0.0 mime: 2.6.0 @@ -34209,7 +34225,7 @@ snapshots: retry-request@4.2.2: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) extend: 3.0.2 transitivePeerDependencies: - supports-color @@ -34646,7 +34662,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -35095,7 +35111,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) fast-safe-stringify: 2.1.1 form-data: 4.0.1 formidable: 3.5.1 @@ -35984,7 +36000,7 @@ snapshots: dependencies: chalk: 2.4.2 commander: 3.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color