feat(msg): add worker threads to behavioural events consumer (#35860)

This commit is contained in:
Meikel Ratz
2025-07-31 15:39:54 +02:00
committed by GitHub
parent 902a9b83f1
commit bcdb26a5e0
5 changed files with 292 additions and 175 deletions

View File

@@ -96,6 +96,7 @@
"p-limit": "3.1.0",
"pg": "^8.6.0",
"pino": "^8.6.0",
"piscina": "^5.1.3",
"posthog-node": "4.14.0",
"pretty-bytes": "^5.6.0",
"prom-client": "^14.2.0",

View File

@@ -8,7 +8,7 @@ import { BehavioralCounterRepository } from '../../utils/db/cassandra/behavioura
import { closeHub, createHub } from '../../utils/db/hub'
import { createIncomingEvent } from '../_tests/fixtures'
import { convertClickhouseRawEventToFilterGlobals } from '../utils/hog-function-filtering'
import { BehavioralEvent, CdpBehaviouralEventsConsumer, counterEventsDropped } from './cdp-behavioural-events.consumer'
import { BehavioralEvent, CdpBehaviouralEventsConsumer } from './cdp-behavioural-events.consumer'
class TestCdpBehaviouralEventsConsumer extends CdpBehaviouralEventsConsumer {
public getCassandraClient(): CassandraClient | null {
@@ -349,45 +349,6 @@ describe('CdpBehaviouralEventsConsumer', () => {
expect(counter).toBeNull()
})
it('should drop events with missing person ID at parsing stage', async () => {
// Create a raw event without person_id (simulating what comes from Kafka)
const rawEventWithoutPersonId = {
uuid: '12345',
event: '$pageview',
team_id: team.id,
properties: JSON.stringify({ $browser: 'Chrome' }),
// person_id is undefined
}
// Get initial metric value
const initialDroppedCount = await counterEventsDropped.get()
const initialMissingPersonIdCount =
initialDroppedCount.values.find((v) => v.labels.reason === 'missing_person_id')?.value || 0
const messages = [
{
value: Buffer.from(JSON.stringify(rawEventWithoutPersonId)),
},
] as any[]
// Act - parse the batch (should drop the event)
const parsedEvents = await (processor as any)._parseKafkaBatch(messages)
// Assert - no events should be parsed due to missing person_id
expect(parsedEvents).toHaveLength(0)
// Assert - metric should be incremented
const finalDroppedCount = await counterEventsDropped.get()
const finalMissingPersonIdCount =
finalDroppedCount.values.find((v) => v.labels.reason === 'missing_person_id')?.value || 0
expect(finalMissingPersonIdCount).toBe(initialMissingPersonIdCount + 1)
// Assert - no counter should be written to Cassandra since event was dropped
const counters = await repository.getCountersForTeam(team.id)
expect(counters).toHaveLength(0)
})
})
})

View File

@@ -1,7 +1,9 @@
import { Client as CassandraClient } from 'cassandra-driver'
import { createHash } from 'crypto'
import { Message } from 'node-rdkafka'
import { Counter } from 'prom-client'
import { join } from 'path'
import Piscina from 'piscina'
import { Counter, Histogram } from 'prom-client'
import { KAFKA_EVENTS_JSON } from '../../config/kafka-topics'
import { KafkaConsumer } from '../../kafka/consumer'
@@ -12,7 +14,6 @@ import { BehavioralCounterRepository, CounterUpdate } from '../../utils/db/cassa
import { parseJSON } from '../../utils/json-parse'
import { logger } from '../../utils/logger'
import { HogFunctionFilterGlobals } from '../types'
import { execHog } from '../utils/hog-exec'
import { convertClickhouseRawEventToFilterGlobals } from '../utils/hog-function-filtering'
import { CdpConsumerBase } from './cdp-base.consumer'
@@ -22,26 +23,35 @@ export type BehavioralEvent = {
personId: string
}
export const counterParseError = new Counter({
name: 'cdp_behavioural_function_parse_error',
help: 'A behavioural function invocation was parsed with an error',
labelNames: ['error'],
export const histogramWorkerPoolExecution = new Histogram({
name: 'cdp_behavioural_worker_pool_execution_duration_ms',
help: 'Time spent executing bytecode in worker pool',
buckets: [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000],
})
export const counterEventsDropped = new Counter({
name: 'cdp_behavioural_events_dropped_total',
help: 'Total number of events dropped due to missing personId or other validation errors',
labelNames: ['reason'],
export const histogramActionLoading = new Histogram({
name: 'cdp_behavioural_action_loading_duration_ms',
help: 'Time spent loading actions for teams',
buckets: [1, 5, 10, 25, 50, 100, 250, 500],
})
export const counterEventsConsumed = new Counter({
name: 'cdp_behavioural_events_consumed_total',
help: 'Total number of events consumed by the behavioural consumer',
export const counterWorkerPoolTasks = new Counter({
name: 'cdp_behavioural_worker_pool_tasks_total',
help: 'Total number of tasks submitted to worker pool',
labelNames: ['status'],
})
export const counterEventsMatchedTotal = new Counter({
name: 'cdp_behavioural_events_matched_total',
help: 'Total number of events that matched at least one action filter',
export const histogramBatchProcessingSteps = new Histogram({
name: 'cdp_behavioural_batch_processing_steps_duration_ms',
help: 'Time spent in different batch processing steps',
labelNames: ['step'],
buckets: [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500],
})
export const histogramActionsPerTeam = new Histogram({
name: 'cdp_behavioural_actions_per_team',
help: 'Number of actions loaded per team',
buckets: [0, 1, 2, 5, 10, 20, 50, 100, 200, 500],
})
export class CdpBehaviouralEventsConsumer extends CdpConsumerBase {
@@ -50,11 +60,23 @@ export class CdpBehaviouralEventsConsumer extends CdpConsumerBase {
protected cassandra: CassandraClient | null
protected behavioralCounterRepository: BehavioralCounterRepository | null
private filterHashCache = new Map<string, string>()
private workerPool: Piscina
constructor(hub: Hub, topic: string = KAFKA_EVENTS_JSON, groupId: string = 'cdp-behavioural-events-consumer') {
super(hub)
this.kafkaConsumer = new KafkaConsumer({ groupId, topic })
// Initialize Piscina worker pool for parallel HOG execution
this.workerPool = new Piscina({
filename: join(__dirname, '../workers/hog-executor.worker.js'),
maxThreads: Math.max(2, Math.floor(require('os').cpus().length * 0.5)),
minThreads: 2,
resourceLimits: {
maxOldGenerationSizeMb: 256,
maxYoungGenerationSizeMb: 64,
},
})
// Only initialize Cassandra client if the feature is enabled
if (hub.WRITE_BEHAVIOURAL_COUNTERS_TO_CASSANDRA) {
this.cassandra = new CassandraClient({
@@ -79,21 +101,19 @@ export class CdpBehaviouralEventsConsumer extends CdpConsumerBase {
return
}
// Track events consumed and matched (absolute numbers)
let eventsMatched = 0
const counterUpdates: CounterUpdate[] = []
const results = await Promise.all(events.map((event) => this.processEvent(event, counterUpdates)))
eventsMatched = results.reduce((sum, count) => sum + count, 0)
// Time event processing
const eventProcessingTimer = histogramBatchProcessingSteps.labels({ step: 'event_processing' }).startTimer()
await Promise.all(events.map((event) => this.processEvent(event, counterUpdates)))
eventProcessingTimer()
// Batch write all counter updates
// Time Cassandra writes
if (counterUpdates.length > 0 && this.hub.WRITE_BEHAVIOURAL_COUNTERS_TO_CASSANDRA && this.cassandra) {
const cassandraTimer = histogramBatchProcessingSteps.labels({ step: 'cassandra_write' }).startTimer()
await this.writeBehavioralCounters(counterUpdates)
cassandraTimer()
}
// Update metrics with absolute numbers
counterEventsConsumed.inc(events.length)
counterEventsMatchedTotal.inc(eventsMatched)
})
}
@@ -121,10 +141,14 @@ export class CdpBehaviouralEventsConsumer extends CdpConsumerBase {
}
private async loadActionsForTeam(teamId: number): Promise<Action[]> {
const timer = histogramActionLoading.startTimer()
try {
const actions = await this.hub.actionManagerCDP.getActionsForTeam(teamId)
timer()
histogramActionsPerTeam.observe(actions.length)
return actions
} catch (error) {
timer()
logger.error('Error loading actions for team', { teamId, error })
return []
}
@@ -140,20 +164,25 @@ export class CdpBehaviouralEventsConsumer extends CdpConsumerBase {
}
try {
// Execute bytecode directly with the filter globals
const execHogOutcome = await execHog(action.bytecode, {
globals: event.filterGlobals,
telemetry: false,
// Execute bytecode in worker thread for better performance
const workerTimer = histogramWorkerPoolExecution.startTimer()
counterWorkerPoolTasks.labels({ status: 'submitted' }).inc()
const result = await this.workerPool.run({
bytecode: action.bytecode,
filterGlobals: event.filterGlobals,
})
if (!execHogOutcome.execResult || execHogOutcome.error || execHogOutcome.execResult.error) {
throw execHogOutcome.error ?? execHogOutcome.execResult?.error ?? new Error('Unknown error')
workerTimer()
if (result.error) {
counterWorkerPoolTasks.labels({ status: 'error' }).inc()
throw result.error
}
const matchedFilter =
typeof execHogOutcome.execResult.result === 'boolean' && execHogOutcome.execResult.result
counterWorkerPoolTasks.labels({ status: 'success' }).inc()
if (matchedFilter) {
if (result.matched) {
const filterHash = this.createFilterHash(action.bytecode!)
const date = new Date().toISOString().split('T')[0]
counterUpdates.push({
@@ -164,9 +193,9 @@ export class CdpBehaviouralEventsConsumer extends CdpConsumerBase {
})
}
return matchedFilter
return result.matched
} catch (error) {
logger.error('Error executing action bytecode', {
logger.error('Error executing action bytecode in worker', {
actionId: String(action.id),
error,
})
@@ -219,7 +248,6 @@ export class CdpBehaviouralEventsConsumer extends CdpConsumerBase {
event: clickHouseEvent.event,
uuid: clickHouseEvent.uuid,
})
counterEventsDropped.labels({ reason: 'missing_person_id' }).inc()
return
}
@@ -233,7 +261,6 @@ export class CdpBehaviouralEventsConsumer extends CdpConsumerBase {
})
} catch (e) {
logger.error('Error parsing message', e)
counterParseError.labels({ error: e.message }).inc()
}
})
// Return Promise.resolve to satisfy runInstrumentedFunction's Promise return type
@@ -275,6 +302,9 @@ export class CdpBehaviouralEventsConsumer extends CdpConsumerBase {
logger.info('💤', 'Stopping behavioural events consumer...')
await this.kafkaConsumer.disconnect()
// Shutdown worker pool
await this.workerPool.destroy()
// Only shutdown Cassandra if it was initialized
if (this.cassandra) {
await this.cassandra.shutdown()

View File

@@ -0,0 +1,37 @@
const { exec } = require('@posthog/hogvm')
const crypto = require('crypto')
const RE2 = require('re2')
module.exports = function execBehavioralActionWorker(task) {
const { bytecode, filterGlobals } = task
const now = performance.now()
let execResult
let error
let matched = false
try {
execResult = exec(bytecode, {
timeout: 30000,
maxAsyncSteps: 0,
globals: filterGlobals,
telemetry: false,
external: {
regex: { match: (regex, str) => new RE2(regex).test(str) },
crypto,
},
})
if (execResult && !execResult.error) {
matched = typeof execResult.result === 'boolean' && execResult.result
}
} catch (e) {
error = e
}
return {
execResult,
error,
durationMs: performance.now() - now,
matched,
}
}

282
pnpm-lock.yaml generated
View File

@@ -1383,6 +1383,9 @@ importers:
pino:
specifier: ^8.6.0
version: 8.11.0
piscina:
specifier: ^5.1.3
version: 5.1.3
posthog-node:
specifier: 4.14.0
version: 4.14.0
@@ -2016,7 +2019,7 @@ importers:
version: 0.26.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@posthog/lemon-ui':
specifier: '*'
version: 0.0.0(antd@5.26.0(date-fns@2.30.0)(luxon@3.5.0)(moment@2.29.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(kea-router@3.3.0(kea@3.1.5(react@18.2.0)))(kea@3.1.5(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
version: 0.0.0(antd@5.26.0(date-fns@2.30.0)(luxon@3.5.0)(moment@2.29.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(kea-router@3.3.1(kea@3.1.5(react@18.2.0)))(kea@3.1.5(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@types/react':
specifier: '*'
version: 17.0.52
@@ -4513,6 +4516,106 @@ packages:
resolution: {integrity: sha512-N8x7eSLGcmUFNWZRxT1vsHvypzIRgQYdG0rJey/rZCy6zT/30qDt8Joj7FxzGNLSwXbeZqJOMqDurp7ra4hgbw==}
engines: {node: '>=14'}
'@napi-rs/nice-android-arm-eabi@1.0.4':
resolution: {integrity: sha512-OZFMYUkih4g6HCKTjqJHhMUlgvPiDuSLZPbPBWHLjKmFTv74COzRlq/gwHtmEVaR39mJQ6ZyttDl2HNMUbLVoA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [android]
'@napi-rs/nice-android-arm64@1.0.4':
resolution: {integrity: sha512-k8u7cjeA64vQWXZcRrPbmwjH8K09CBnNaPnI9L1D5N6iMPL3XYQzLcN6WwQonfcqCDv5OCY3IqX89goPTV4KMw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@napi-rs/nice-darwin-arm64@1.0.4':
resolution: {integrity: sha512-GsLdQvUcuVzoyzmtjsThnpaVEizAqH5yPHgnsBmq3JdVoVZHELFo7PuJEdfOH1DOHi2mPwB9sCJEstAYf3XCJA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@napi-rs/nice-darwin-x64@1.0.4':
resolution: {integrity: sha512-1y3gyT3e5zUY5SxRl3QDtJiWVsbkmhtUHIYwdWWIQ3Ia+byd/IHIEpqAxOGW1nhhnIKfTCuxBadHQb+yZASVoA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@napi-rs/nice-freebsd-x64@1.0.4':
resolution: {integrity: sha512-06oXzESPRdXUuzS8n2hGwhM2HACnDfl3bfUaSqLGImM8TA33pzDXgGL0e3If8CcFWT98aHows5Lk7xnqYNGFeA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
'@napi-rs/nice-linux-arm-gnueabihf@1.0.4':
resolution: {integrity: sha512-CgklZ6g8WL4+EgVVkxkEvvsi2DSLf9QIloxWO0fvQyQBp6VguUSX3eHLeRpqwW8cRm2Hv/Q1+PduNk7VK37VZw==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@napi-rs/nice-linux-arm64-gnu@1.0.4':
resolution: {integrity: sha512-wdAJ7lgjhAlsANUCv0zi6msRwq+D4KDgU+GCCHssSxWmAERZa2KZXO0H2xdmoJ/0i03i6YfK/sWaZgUAyuW2oQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@napi-rs/nice-linux-arm64-musl@1.0.4':
resolution: {integrity: sha512-4b1KYG+sriufhFrpUS9uNOEYYJqSfcbnwGx6uGX7JjrH8tELG90cOpCawz5THNIwlS3DhLgnCOcn0+4p6z26QA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@napi-rs/nice-linux-ppc64-gnu@1.0.4':
resolution: {integrity: sha512-iaf3vMRgr23oe1PUaKpxaH3DS0IMN0+N9iEiWVwYPm/U15vZFYdqVegGfN2PzrZLUl5lc8ZxbmEKDfuqslhAMA==}
engines: {node: '>= 10'}
cpu: [ppc64]
os: [linux]
'@napi-rs/nice-linux-riscv64-gnu@1.0.4':
resolution: {integrity: sha512-UXoREY6Yw6rHrGuTwQgBxpfjK34t6mTjibE9/cXbefL9AuUCJ9gEgwNKZiONuR5QGswChqo9cnthjdKkYyAdDg==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
'@napi-rs/nice-linux-s390x-gnu@1.0.4':
resolution: {integrity: sha512-eFbgYCRPmsqbYPAlLYU5hYTNbogmIDUvknilehHsFhCH1+0/kN87lP+XaLT0Yeq4V/rpwChSd9vlz4muzFArtw==}
engines: {node: '>= 10'}
cpu: [s390x]
os: [linux]
'@napi-rs/nice-linux-x64-gnu@1.0.4':
resolution: {integrity: sha512-4T3E6uTCwWT6IPnwuPcWVz3oHxvEp/qbrCxZhsgzwTUBEwu78EGNXGdHfKJQt3soth89MLqZJw+Zzvnhrsg1mQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@napi-rs/nice-linux-x64-musl@1.0.4':
resolution: {integrity: sha512-NtbBkAeyBPLvCBkWtwkKXkNSn677eaT0cX3tygq+2qVv71TmHgX4gkX6o9BXjlPzdgPGwrUudavCYPT9tzkEqQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@napi-rs/nice-win32-arm64-msvc@1.0.4':
resolution: {integrity: sha512-vubOe3i+YtSJGEk/++73y+TIxbuVHi+W8ZzrRm2eETCjCRwNlgbfToQZ85dSA+4iBB/NJRGNp+O4hfdbbttZWA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@napi-rs/nice-win32-ia32-msvc@1.0.4':
resolution: {integrity: sha512-BMOVrUDZeg1RNRKVlh4eyLv5djAAVLiSddfpuuQ47EFjBcklg0NUeKMFKNrKQR4UnSn4HAiACLD7YK7koskwmg==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@napi-rs/nice-win32-x64-msvc@1.0.4':
resolution: {integrity: sha512-kCNk6HcRZquhw/whwh4rHsdPyOSCQCgnVDVik+Y9cuSVTDy3frpiCJTScJqPPS872h4JgZKkr/+CwcwttNEo9Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@napi-rs/nice@1.0.4':
resolution: {integrity: sha512-Sqih1YARrmMoHlXGgI9JrrgkzxcaaEso0AH+Y7j8NHonUs+xe4iDsgC3IBIDNdzEewbNpccNN6hip+b5vmyRLw==}
engines: {node: '>= 10'}
'@napi-rs/snappy-android-arm-eabi@7.2.2':
resolution: {integrity: sha512-H7DuVkPCK5BlAr1NfSU8bDEN7gYs+R78pSHhDng83QxRnCLmVIZk33ymmIwurmoA1HrdTxbkbuNl+lMvNqnytw==}
engines: {node: '>= 10'}
@@ -12240,11 +12343,6 @@ packages:
peerDependencies:
kea: '>= 3'
kea-router@3.3.0:
resolution: {integrity: sha512-lcun+z/MFVaK/FK6rFSr1/Epu/cpHir1X7nW9Cr4XMBbbNS6t0MA0SsgAiKY35eM5Vd6c0Y33uI2BIWg/dXBfA==}
peerDependencies:
kea: '>= 3'
kea-router@3.3.1:
resolution: {integrity: sha512-SA1RiFbWalvJ1u8pRZf8zACvm2UP1fLIr7Jq8uyIvWQov2V0xyh28pI6TgndJUeSp4i1yoGlA6IMmCHoi0q2nA==}
peerDependencies:
@@ -13607,6 +13705,10 @@ packages:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'}
piscina@5.1.3:
resolution: {integrity: sha512-0u3N7H4+hbr40KjuVn2uNhOcthu/9usKhnw5vT3J7ply79v3D3M8naI00el9Klcy16x557VsEkkUQaHCWFXC/g==}
engines: {node: '>=20.x'}
pixelmatch@5.3.0:
resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==}
hasBin: true
@@ -20415,41 +20517,6 @@ snapshots:
jest-util: 29.7.0
slash: 3.0.0
'@jest/core@29.7.0':
dependencies:
'@jest/console': 29.7.0
'@jest/reporters': 29.7.0
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.15.17
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 3.8.0
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.7.0
jest-config: 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))
jest-haste-map: 29.7.0
jest-message-util: 29.7.0
jest-regex-util: 29.6.3
jest-resolve: 29.7.0
jest-resolve-dependencies: 29.7.0
jest-runner: 29.7.0
jest-runtime: 29.7.0
jest-snapshot: 29.7.0
jest-util: 29.7.0
jest-validate: 29.7.0
jest-watcher: 29.7.0
micromatch: 4.0.8
pretty-format: 29.7.0
slash: 3.0.0
strip-ansi: 6.0.1
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
- ts-node
'@jest/core@29.7.0(ts-node@10.9.1(@swc/core@1.10.14(@swc/helpers@0.5.15))(@types/node@22.15.17)(typescript@5.2.2))':
dependencies:
'@jest/console': 29.7.0
@@ -20834,6 +20901,74 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@napi-rs/nice-android-arm-eabi@1.0.4':
optional: true
'@napi-rs/nice-android-arm64@1.0.4':
optional: true
'@napi-rs/nice-darwin-arm64@1.0.4':
optional: true
'@napi-rs/nice-darwin-x64@1.0.4':
optional: true
'@napi-rs/nice-freebsd-x64@1.0.4':
optional: true
'@napi-rs/nice-linux-arm-gnueabihf@1.0.4':
optional: true
'@napi-rs/nice-linux-arm64-gnu@1.0.4':
optional: true
'@napi-rs/nice-linux-arm64-musl@1.0.4':
optional: true
'@napi-rs/nice-linux-ppc64-gnu@1.0.4':
optional: true
'@napi-rs/nice-linux-riscv64-gnu@1.0.4':
optional: true
'@napi-rs/nice-linux-s390x-gnu@1.0.4':
optional: true
'@napi-rs/nice-linux-x64-gnu@1.0.4':
optional: true
'@napi-rs/nice-linux-x64-musl@1.0.4':
optional: true
'@napi-rs/nice-win32-arm64-msvc@1.0.4':
optional: true
'@napi-rs/nice-win32-ia32-msvc@1.0.4':
optional: true
'@napi-rs/nice-win32-x64-msvc@1.0.4':
optional: true
'@napi-rs/nice@1.0.4':
optionalDependencies:
'@napi-rs/nice-android-arm-eabi': 1.0.4
'@napi-rs/nice-android-arm64': 1.0.4
'@napi-rs/nice-darwin-arm64': 1.0.4
'@napi-rs/nice-darwin-x64': 1.0.4
'@napi-rs/nice-freebsd-x64': 1.0.4
'@napi-rs/nice-linux-arm-gnueabihf': 1.0.4
'@napi-rs/nice-linux-arm64-gnu': 1.0.4
'@napi-rs/nice-linux-arm64-musl': 1.0.4
'@napi-rs/nice-linux-ppc64-gnu': 1.0.4
'@napi-rs/nice-linux-riscv64-gnu': 1.0.4
'@napi-rs/nice-linux-s390x-gnu': 1.0.4
'@napi-rs/nice-linux-x64-gnu': 1.0.4
'@napi-rs/nice-linux-x64-musl': 1.0.4
'@napi-rs/nice-win32-arm64-msvc': 1.0.4
'@napi-rs/nice-win32-ia32-msvc': 1.0.4
'@napi-rs/nice-win32-x64-msvc': 1.0.4
optional: true
'@napi-rs/snappy-android-arm-eabi@7.2.2':
optional: true
@@ -21754,11 +21889,11 @@ snapshots:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
'@posthog/lemon-ui@0.0.0(antd@5.26.0(date-fns@2.30.0)(luxon@3.5.0)(moment@2.29.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(kea-router@3.3.0(kea@3.1.5(react@18.2.0)))(kea@3.1.5(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
'@posthog/lemon-ui@0.0.0(antd@5.26.0(date-fns@2.30.0)(luxon@3.5.0)(moment@2.29.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(kea-router@3.3.1(kea@3.1.5(react@18.2.0)))(kea@3.1.5(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
antd: 5.26.0(date-fns@2.30.0)(luxon@3.5.0)(moment@2.29.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
kea: 3.1.5(react@18.2.0)
kea-router: 3.3.0(kea@3.1.5(react@18.2.0))
kea-router: 3.3.1(kea@3.1.5(react@18.2.0))
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -24334,7 +24469,7 @@ snapshots:
commander: 9.4.1
expect-playwright: 0.8.0
glob: 10.4.5
jest: 29.7.0
jest: 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))
jest-circus: 29.7.0
jest-environment-node: 29.7.0
jest-junit: 16.0.0
@@ -27186,21 +27321,6 @@ snapshots:
safe-buffer: 5.2.1
sha.js: 2.4.11
create-jest@29.7.0:
dependencies:
'@jest/types': 29.6.3
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
jest-config: 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))
jest-util: 29.7.0
prompts: 2.4.2
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
create-jest@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)):
dependencies:
'@jest/types': 29.6.3
@@ -30184,25 +30304,6 @@ snapshots:
- babel-plugin-macros
- supports-color
jest-cli@29.7.0:
dependencies:
'@jest/core': 29.7.0
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
create-jest: 29.7.0
exit: 0.1.2
import-local: 3.1.0
jest-config: 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))
jest-util: 29.7.0
jest-validate: 29.7.0
yargs: 17.7.1
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
jest-cli@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)):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.1(@swc/core@1.10.14(@swc/helpers@0.5.15))(@types/node@22.15.17)(typescript@5.2.2))
@@ -30417,7 +30518,7 @@ snapshots:
jest-playwright-preset@4.0.0(jest-circus@29.7.0)(jest-environment-node@29.7.0)(jest-runner@29.7.0)(jest@29.7.0):
dependencies:
expect-playwright: 0.8.0
jest: 29.7.0
jest: 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))
jest-circus: 29.7.0
jest-environment-node: 29.7.0
jest-process-manager: 0.4.0
@@ -30575,7 +30676,7 @@ snapshots:
dependencies:
ansi-escapes: 6.0.0
chalk: 5.4.1
jest: 29.7.0
jest: 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))
jest-regex-util: 29.6.3
jest-watcher: 29.7.0
slash: 5.1.0
@@ -30606,18 +30707,6 @@ snapshots:
merge-stream: 2.0.0
supports-color: 8.1.1
jest@29.7.0:
dependencies:
'@jest/core': 29.7.0
'@jest/types': 29.6.3
import-local: 3.1.0
jest-cli: 29.7.0
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
jest@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)):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.1(@swc/core@1.10.14(@swc/helpers@0.5.15))(@types/node@22.15.17)(typescript@5.2.2))
@@ -30817,11 +30906,6 @@ snapshots:
dependencies:
kea: 3.1.5(react@18.2.0)
kea-router@3.3.0(kea@3.1.5(react@18.2.0)):
dependencies:
kea: 3.1.5(react@18.2.0)
url-pattern: 1.0.3
kea-router@3.3.1(kea@3.1.5(react@18.2.0)):
dependencies:
kea: 3.1.5(react@18.2.0)
@@ -32331,6 +32415,10 @@ snapshots:
pirates@4.0.6: {}
piscina@5.1.3:
optionalDependencies:
'@napi-rs/nice': 1.0.4
pixelmatch@5.3.0:
dependencies:
pngjs: 6.0.0