diff --git a/benchmarks/postgres/ingestion.benchmark.ts b/benchmarks/postgres/ingestion.benchmark.ts index d7e80adb..9c687411 100644 --- a/benchmarks/postgres/ingestion.benchmark.ts +++ b/benchmarks/postgres/ingestion.benchmark.ts @@ -52,7 +52,6 @@ describe('ingestion benchmarks', () => { LOG_LEVEL: LogLevel.Log, }) eventsProcessor = new EventsProcessor(hub) - await eventsProcessor.prepare() team = await getFirstTeam(hub) now = DateTime.utc() diff --git a/package.json b/package.json index 49b8eee3..d009e247 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@types/lru-cache": "^5.1.0", "adm-zip": "^0.4.16", "aws-sdk": "^2.884.0", + "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "generic-pool": "^3.7.1", "graphile-worker": "^0.11.1", @@ -75,6 +76,7 @@ "posthog-js-lite": "^0.0.5", "pretty-bytes": "^5.6.0", "protobufjs": "^6.10.2", + "re2": "^1.16.0", "redlock": "^4.2.0", "snowflake-sdk": "^1.6.0", "tar-stream": "^2.1.4", diff --git a/src/config/config.ts b/src/config/config.ts index 0219953e..8ea6751f 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -73,6 +73,7 @@ export function getDefaultConfig(): PluginsServerConfig { CRASH_IF_NO_PERSISTENT_JOB_QUEUE: false, STALENESS_RESTART_SECONDS: 0, CAPTURE_INTERNAL_METRICS: false, + PLUGIN_SERVER_ACTION_MATCHING: 0, } } @@ -124,6 +125,8 @@ export function getConfigHelp(): Record { 'refuse to start unless there is a properly configured persistent job queue (e.g. graphile)', STALENESS_RESTART_SECONDS: 'trigger a restart if no event ingested for this duration', CAPTURE_INTERNAL_METRICS: 'capture internal metrics for posthog in posthog', + PLUGIN_SERVER_ACTION_MATCHING: + 'whether plugin server action matching results should be used (transition period setting)', } } diff --git a/src/types.ts b/src/types.ts index a92024fa..c5b0749f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,8 @@ import { DB } from './utils/db/db' import { KafkaProducerWrapper } from './utils/db/kafka-producer-wrapper' import { InternalMetrics } from './utils/internal-metrics' import { UUID } from './utils/utils' +import { ActionManager } from './worker/ingestion/action-manager' +import { ActionMatcher } from './worker/ingestion/action-matcher' import { EventsProcessor } from './worker/ingestion/process-event' import { LazyPluginVM } from './worker/vm/lazy' @@ -91,6 +93,7 @@ export interface PluginsServerConfig extends Record { CRASH_IF_NO_PERSISTENT_JOB_QUEUE: boolean STALENESS_RESTART_SECONDS: number CAPTURE_INTERNAL_METRICS: boolean + PLUGIN_SERVER_ACTION_MATCHING: 0 | 1 | 2 } export interface Hub extends PluginsServerConfig { @@ -115,6 +118,8 @@ export interface Hub extends PluginsServerConfig { pluginConfigSecrets: Map pluginConfigSecretLookup: Map // tools + actionManager: ActionManager + actionMatcher: ActionMatcher eventsProcessor: EventsProcessor jobQueueManager: JobQueueManager // diagnostics @@ -487,44 +492,43 @@ export enum PropertyOperator { } /** Sync with posthog/frontend/src/types.ts */ -interface BasePropertyFilter { +interface PropertyFilterBase { key: string value: string | number | Array | null label?: string } /** Sync with posthog/frontend/src/types.ts */ -export interface EventPropertyFilter extends BasePropertyFilter { - type: 'event' +export interface PropertyFilterWithOperator extends PropertyFilterBase { operator: PropertyOperator } /** Sync with posthog/frontend/src/types.ts */ -export interface PersonPropertyFilter extends BasePropertyFilter { +export interface EventPropertyFilter extends PropertyFilterWithOperator { + type: 'event' +} + +/** Sync with posthog/frontend/src/types.ts */ +export interface PersonPropertyFilter extends PropertyFilterWithOperator { type: 'person' - operator: PropertyOperator } /** Sync with posthog/frontend/src/types.ts */ -export interface ElementPropertyFilter extends BasePropertyFilter { +export interface ElementPropertyFilter extends PropertyFilterWithOperator { type: 'element' key: 'tag_name' | 'text' | 'href' | 'selector' - operator: PropertyOperator + value: string } /** Sync with posthog/frontend/src/types.ts */ -export interface CohortPropertyFilter extends BasePropertyFilter { +export interface CohortPropertyFilter extends PropertyFilterBase { type: 'cohort' key: 'id' - value: number + value: number | string } /** Sync with posthog/frontend/src/types.ts */ -export type ActionStepProperties = - | EventPropertyFilter - | PersonPropertyFilter - | ElementPropertyFilter - | CohortPropertyFilter +export type PropertyFilter = EventPropertyFilter | PersonPropertyFilter | ElementPropertyFilter | CohortPropertyFilter /** Sync with posthog/frontend/src/types.ts */ export enum ActionStepUrlMatching { @@ -544,7 +548,7 @@ export interface ActionStep { url_matching: ActionStepUrlMatching | null name: string | null event: string | null - properties: ActionStepProperties[] | null + properties: PropertyFilter[] | null } /** Raw Action row from database. */ diff --git a/src/utils/db/db.ts b/src/utils/db/db.ts index 0ac03844..10821543 100644 --- a/src/utils/db/db.ts +++ b/src/utils/db/db.ts @@ -536,6 +536,17 @@ export class DB { } } + // Cohort & CohortPeople + + public async doesPersonBelongToCohort(cohortId: number, personId: Person['id']): Promise { + const selectResult = await this.postgresQuery( + `SELECT EXISTS (SELECT 1 FROM posthog_cohortpeople WHERE cohort_id = $1 AND person_id = $2);`, + [cohortId, personId], + 'doesPersonBelongToCohort' + ) + return selectResult.rows[0] + } + // Organization public async fetchOrganization(organizationId: string): Promise { @@ -724,7 +735,7 @@ export class DB { ).rows as PropertyDefinitionType[] } - // Action & ActionStep + // Action & ActionStep & Action<>Event public async fetchAllActionsGroupedByTeam(): Promise>> { const rawActions: RawAction[] = ( @@ -772,6 +783,15 @@ export class DB { return action } + public async registerEventActionOccurrences(eventId: Event['id'], actions: Action[]): Promise { + const valuesClause = actions.map((action, index) => `($1, $${index + 2})`).join(', ') + await this.postgresQuery( + `INSERT INTO posthog_action_events (event_id, action_id) VALUES ${valuesClause}`, + [eventId, ...actions.map((action) => action.id)], + 'registerEventActionOccurrences' + ) + } + // Team Internal Metrics public async fetchInternalMetricsTeam(): Promise { diff --git a/src/utils/db/hub.ts b/src/utils/db/hub.ts index 9e7b50f4..e1f8aca0 100644 --- a/src/utils/db/hub.ts +++ b/src/utils/db/hub.ts @@ -13,6 +13,8 @@ import { ConnectionOptions } from 'tls' import { defaultConfig } from '../../config/config' import { JobQueueManager } from '../../main/job-queues/job-queue-manager' import { Hub, PluginsServerConfig } from '../../types' +import { ActionManager } from '../../worker/ingestion/action-manager' +import { ActionMatcher } from '../../worker/ingestion/action-matcher' import { EventsProcessor } from '../../worker/ingestion/process-event' import { InternalMetrics } from '../internal-metrics' import { killProcess } from '../kill' @@ -175,8 +177,10 @@ export async function createHub( } // :TODO: This is only used on worker threads, not main + hub.actionManager = new ActionManager(db) + await hub.actionManager.prepare() + hub.actionMatcher = new ActionMatcher(db, hub.actionManager, statsd) hub.eventsProcessor = new EventsProcessor(hub as Hub) - await hub.eventsProcessor.prepare() hub.jobQueueManager = new JobQueueManager(hub as Hub) if (serverConfig.CAPTURE_INTERNAL_METRICS) { diff --git a/src/utils/db/utils.ts b/src/utils/db/utils.ts index 00280239..c7a9fb4e 100644 --- a/src/utils/db/utils.ts +++ b/src/utils/db/utils.ts @@ -158,6 +158,19 @@ export function chainToElements(chain: string): Element[] { return elements } +export function extractElements(elements: Record[]): Element[] { + return elements.map((el) => ({ + text: el['$el_text']?.slice(0, 400), + tag_name: el['tag_name'], + href: el['attr__href']?.slice(0, 2048), + attr_class: el['attr__class']?.split(' '), + attr_id: el['attr__id'], + nth_child: el['nth_child'], + nth_of_type: el['nth_of_type'], + attributes: Object.fromEntries(Object.entries(el).filter(([key]) => key.startsWith('attr__'))), + })) +} + export function timeoutGuard( message: string, context?: Record, diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 48b60455..ecf0cef5 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -10,7 +10,7 @@ import { Readable } from 'stream' import * as tar from 'tar-stream' import * as zlib from 'zlib' -import { LogLevel, Plugin, PluginConfigId, PluginsServerConfig, TimestampFormat } from '../types' +import { Element, LogLevel, Plugin, PluginConfigId, PluginsServerConfig, TimestampFormat } from '../types' import { status } from './status' /** Time until autoexit (due to error) gives up on graceful exit and kills the process right away. */ diff --git a/src/worker/ingestion/action-manager.ts b/src/worker/ingestion/action-manager.ts index 84fb5231..f5212f6c 100644 --- a/src/worker/ingestion/action-manager.ts +++ b/src/worker/ingestion/action-manager.ts @@ -21,11 +21,11 @@ export class ActionManager { this.ready = true } - public getTeamActions(teamId: Team['id']): ActionMap | null { + public getTeamActions(teamId: Team['id']): ActionMap { if (!this.ready) { throw new Error('ActionManager is not ready! Run actionManager.prepare() before this') } - return this.actionCache[teamId] || null + return this.actionCache[teamId] || {} } public async reloadAllActions(): Promise { @@ -35,7 +35,15 @@ export class ActionManager { public async reloadAction(teamId: Team['id'], actionId: Action['id']): Promise { const refetchedAction = await this.db.fetchAction(actionId) - const wasCachedAlready = teamId in this.actionCache && actionId in this.actionCache[teamId] + + let wasCachedAlready = true + if (!this.actionCache[teamId]) { + wasCachedAlready = false + this.actionCache[teamId] = {} + } else if (!this.actionCache[teamId][actionId]) { + wasCachedAlready = false + } + if (refetchedAction) { status.debug( '🍿', @@ -59,7 +67,8 @@ export class ActionManager { } public dropAction(teamId: Team['id'], actionId: Action['id']): void { - const wasCachedAlready = teamId in this.actionCache && actionId in this.actionCache[teamId] + const wasCachedAlready = !!this.actionCache?.[teamId]?.[actionId] + if (wasCachedAlready) { status.info('🍿', `Deleted action ID ${actionId} (team ID ${teamId}) from cache`) delete this.actionCache[teamId][actionId] diff --git a/src/worker/ingestion/action-matcher.ts b/src/worker/ingestion/action-matcher.ts new file mode 100644 index 00000000..bad888b0 --- /dev/null +++ b/src/worker/ingestion/action-matcher.ts @@ -0,0 +1,512 @@ +import { PluginEvent } from '@posthog/plugin-scaffold' +import { Properties } from '@posthog/plugin-scaffold/src/types' +import escapeStringRegexp from 'escape-string-regexp' +import equal from 'fast-deep-equal' +import { StatsD } from 'hot-shots' +import RE2 from 're2' + +import { + Action, + ActionStep, + ActionStepUrlMatching, + CohortPropertyFilter, + Element, + ElementPropertyFilter, + EventPropertyFilter, + Person, + PersonPropertyFilter, + PluginsServerConfig, + PropertyFilter, + PropertyFilterWithOperator, + PropertyOperator, +} from '../../types' +import { DB } from '../../utils/db/db' +import { extractElements } from '../../utils/db/utils' +import { ActionManager } from './action-manager' + +const propertyOperatorToRequiredValueType: Partial> = { + [PropertyOperator.IContains]: 'string', + [PropertyOperator.NotIContains]: 'string', + [PropertyOperator.Regex]: 'string', + [PropertyOperator.NotRegex]: 'string', +} + +export class ActionMatcher { + private db: DB + private actionManager: ActionManager + private statsd: StatsD | undefined + + constructor(db: DB, actionManager: ActionManager, statsd?: StatsD) { + this.db = db + this.actionManager = actionManager + this.statsd = statsd + } + + /** Get all actions matched to the event. */ + public async match(event: PluginEvent, person?: Person, elements?: Element[]): Promise { + const matchingStart = new Date() + const teamActions: Action[] = Object.values(this.actionManager.getTeamActions(event.team_id)) + if (!elements) { + const rawElements: Record[] | undefined = event.properties?.['$elements'] + elements = rawElements ? extractElements(rawElements) : [] + } + const teamActionsMatching: boolean[] = await Promise.all( + teamActions.map((action) => this.checkAction(event, elements, person, action)) + ) + const matches: Action[] = [] + for (let i = 0; i < teamActionsMatching.length; i++) { + if (teamActionsMatching[i]) { + matches.push(teamActions[i]) + } + } + this.statsd?.timing('action_matching_for_event', matchingStart) + this.statsd?.increment('action_matches_found', matches.length) + return matches + } + + /** + * Base level of action matching. + * + * Return whether the event is a match for the action. + * The event is considered a match if any of the action's steps (match groups) is a match. + */ + public async checkAction( + event: PluginEvent, + elements: Element[] | undefined, + person: Person | undefined, + action: Action + ): Promise { + for (const step of action.steps) { + if (await this.checkStep(event, elements, person, step)) { + return true + } + } + return false + } + + /** + * Sublevel 1 of action matching. + * + * Return whether the event is a match for the step (match group). + * The event is considered a match if no subcheck fails. Many subchecks are usually irrelevant and skipped. + */ + private async checkStep( + event: PluginEvent, + elements: Element[] | undefined, + person: Person | undefined, + step: ActionStep + ): Promise { + if (!elements) { + elements = [] + } + return ( + this.checkStepElement(elements, step) && + this.checkStepUrl(event, step) && + this.checkStepEvent(event, step) && + (await this.checkStepFilters(event, elements, person, step)) + ) + } + + /** + * Sublevel 2 of action matching. + * + * Return whether the event is a match for the step's "URL" constraint. + * Step properties: `url_matching`, `url`. + */ + private checkStepUrl(event: PluginEvent, step: ActionStep): boolean { + // CHECK CONDITIONS, OTHERWISE SKIPPED + if (step.url_matching && step.url) { + const eventUrl = event.properties?.$current_url + if (!eventUrl || typeof eventUrl !== 'string') { + return false // URL IS UNKNOWN + } + let doesUrlMatch: boolean + switch (step.url_matching) { + case ActionStepUrlMatching.Contains: + // Simulating SQL LIKE behavior (_ = any single character, % = any zero or more characters) + const adjustedRegExpString = escapeStringRegexp(step.url).replace(/_/g, '.').replace(/%/g, '.*') + doesUrlMatch = new RegExp(adjustedRegExpString).test(eventUrl) + break + case ActionStepUrlMatching.Regex: + // Using RE2 here because that's what ClickHouse uses for regex matching anyway + // It's also safer for user-provided patterns because of a few explicit limitations + doesUrlMatch = new RE2(step.url).test(eventUrl) + break + case ActionStepUrlMatching.Exact: + doesUrlMatch = step.url === eventUrl + break + default: + throw new Error(`Unrecognized ActionStep.url_matching value ${step.url_matching}!`) + } + if (!doesUrlMatch) { + return false // URL IS A MISMATCH + } + } + return true + } + + /** + * Sublevel 2 of action matching. + * + * Return whether the event is a match for + * the step's "Link href equals", "Text equals" and "HTML selector matches" constraints. + * Step properties: `tag_name`, `text`, `href`, `selector`. + */ + private checkStepElement(elements: Element[], step: ActionStep): boolean { + // CHECK CONDITIONS, OTHERWISE SKIPPED + if (step.href || step.tag_name || step.text) { + if ( + !elements.some((element) => { + if (step.href && element.href !== step.href) { + return false // ELEMENT HREF IS A MISMATCH + } + if (step.tag_name && element.tag_name !== step.tag_name) { + return false // ELEMENT TAG NAME IS A MISMATCH + } + if (step.text && element.text !== step.text) { + return false // ELEMENT TEXT IS A MISMATCH + } + return true + }) + ) { + // AT LEAST ONE ELEMENT MUST BE A SUBMATCH + return false + } + } + if (step.selector && !this.checkElementsAgainstSelector(elements, step.selector)) { + return false // SELECTOR IS A MISMATCH + } + return true + } + + /** + * Sublevel 2 of action matching. + * + * Return whether the event is a match for the step's event name constraint. + * Step property: `event`. + */ + private checkStepEvent(event: PluginEvent, step: ActionStep): boolean { + // CHECK CONDITIONS, OTHERWISE SKIPPED + if (step.event && event.event !== step.event) { + return false // EVENT NAME IS A MISMATCH + } + return true + } + + /** + * Sublevel 2 of action matching. + * + * Return whether the event is a match for the step's fiter constraints. + * Step property: `properties`. + */ + private async checkStepFilters( + event: PluginEvent, + elements: Element[], + person: Person | undefined, + step: ActionStep + ): Promise { + // CHECK CONDITIONS, OTHERWISE SKIPPED, OTHERWISE SKIPPED + if (step.properties && step.properties.length) { + // EVERY FILTER MUST BE A MATCH + for (const filter of step.properties) { + if (!(await this.checkEventAgainstFilter(event, elements, person, filter))) { + return false + } + } + } + return true + } + + /** + * Sublevel 3 of action matching. + */ + private async checkEventAgainstFilter( + event: PluginEvent, + elements: Element[], + person: Person | undefined, + filter: PropertyFilter + ): Promise { + switch (filter.type) { + case 'event': + return this.checkEventAgainstEventFilter(event, filter) + case 'person': + if (!person) { + return false + } + return this.checkEventAgainstPersonFilter(person, filter) + case 'element': + return this.checkEventAgainstElementFilter(elements, filter) + case 'cohort': + if (!person) { + return false + } + return await this.checkEventAgainstCohortFilter(person, filter) + default: + return false + } + } + + /** + * Sublevel 4 of action matching. + */ + private checkEventAgainstEventFilter(event: PluginEvent, filter: EventPropertyFilter): boolean { + return this.checkPropertiesAgainstFilter(event.properties, filter) + } + + /** + * Sublevel 4 of action matching. + */ + private checkEventAgainstPersonFilter(person: Person, filter: PersonPropertyFilter): boolean { + if (!person.properties) { + return false + } + return this.checkPropertiesAgainstFilter(person.properties, filter) + } + + /** + * Sublevel 4 of action matching. + */ + private checkEventAgainstElementFilter(elements: Element[], filter: ElementPropertyFilter): boolean { + if (filter.key === 'selector') { + return this.checkElementsAgainstSelector(elements, filter.value) + } else { + return elements.some((element) => this.checkPropertiesAgainstFilter(element, filter)) + } + } + + /** + * Sublevel 4 of action matching. + */ + private async checkEventAgainstCohortFilter(person: Person, filter: CohortPropertyFilter): Promise { + if (!person.properties) { + return false + } + let cohortId = filter.value + if (typeof cohortId !== 'number') { + cohortId = parseInt(cohortId) + } + if (isNaN(cohortId)) { + throw new Error(`Can't match against invalid cohort ID value "${filter.value}!"`) + } + return await this.db.doesPersonBelongToCohort(Number(filter.value), person.id) + } + + /** + * Sublevel 5 of action matching. + */ + private checkPropertiesAgainstFilter( + properties: Properties | null | undefined, + filter: PropertyFilterWithOperator + ): boolean { + if (!properties) { + return false // MISMATCH DUE TO LACK OF PROPERTIES THAT COULD FULFILL CONDITION + } + + const possibleValues = Array.isArray(filter.value) ? filter.value : [filter.value] + const foundValue = properties[filter.key] + let foundValueLowerCase: string // only calculated if needed for a case-insensitive operator + + const requiredValueType = propertyOperatorToRequiredValueType[filter.operator] + if (requiredValueType && typeof foundValue !== requiredValueType) { + return false // MISMATCH DUE TO VALUE TYPE INCOMPATIBLE WITH OPERATOR SUPPORT + } + + let test: (possibleValue: any) => boolean + switch (filter.operator) { + case PropertyOperator.Exact: + test = (possibleValue) => possibleValue === foundValue + break + case PropertyOperator.IsNot: + test = (possibleValue) => possibleValue !== foundValue + break + case PropertyOperator.IContains: + foundValueLowerCase = foundValue.toLowerCase() + test = (possibleValue) => + typeof possibleValue === 'string' && foundValueLowerCase.includes(possibleValue.toLowerCase()) + break + case PropertyOperator.NotIContains: + foundValueLowerCase = foundValue.toLowerCase() + test = (possibleValue) => + typeof possibleValue === 'string' && !foundValueLowerCase.includes(possibleValue.toLowerCase()) + break + case PropertyOperator.Regex: + test = (possibleValue) => typeof possibleValue === 'string' && new RE2(possibleValue).test(foundValue) + break + case PropertyOperator.NotRegex: + test = (possibleValue) => typeof possibleValue === 'string' && !new RE2(possibleValue).test(foundValue) + break + case PropertyOperator.GreaterThan: + test = (possibleValue) => foundValue > possibleValue + break + case PropertyOperator.LessThan: + test = (possibleValue) => foundValue < possibleValue + break + case PropertyOperator.IsSet: + test = () => foundValue !== undefined + break + case PropertyOperator.IsNotSet: + test = () => foundValue === undefined + break + default: + throw new Error( + `Operator ${filter.operator} is unknown and can't be used for event property filtering!` + ) + } + + return possibleValues.some(test) // ANY OF POSSIBLE VALUES MUST BE A MATCH AGAINST THE FOUND VALUE + } + + /** + * Sublevel 3 or 5 of action matching. + */ + public checkElementsAgainstSelector(elements: Element[], selector: string, escapeSlashes = true): boolean { + const parts: SelectorPart[] = [] + // Sometimes people manually add *, just remove them as they don't do anything + selector = selector + .replace(/> \* > /g, '') + .replace(/> \*/g, '') + .trim() + const tags = selector.split(' ') + // Detecting selector parts + for (let partIndex = 0; partIndex < tags.length; partIndex++) { + const tag = tags[partIndex] + if (tag === '>' || tag === '') { + continue + } + const directDescendant = partIndex > 0 && tags[partIndex - 1] === '>' + const part = new SelectorPart(tag, directDescendant, escapeSlashes) + part.uniqueOrder = parts.filter((p) => equal(p.requirements, part.requirements)).length + parts.push(part) + } + // Matching elements against selector parts + // Initial base element is the imaginary parent of the outermost known element + let baseElementIndex = elements.length + let wasPartMatched = true + let partIndex = 0 + while (partIndex < parts.length) { + if (baseElementIndex <= 0) { + // At least one part wasn't matched yet, but at the same time there are no more matchable elements left! + return false + } + const part = parts[partIndex] + wasPartMatched = false + for (let depthDiff = 1; baseElementIndex - depthDiff >= 0; depthDiff++) { + // Subtracting depthDiff as elements are reversed, meaning outer elements have higher indexes + const currentElementIndex = baseElementIndex - depthDiff + if ( + this.checkElementAgainstSelectorPartRequirements(elements[currentElementIndex], part.requirements) + ) { + baseElementIndex = currentElementIndex + wasPartMatched = true + break + } else if (part.directDescendant) { + break + } + } + if (wasPartMatched) { + partIndex++ + } else { + if (part.directDescendant) { + // For a direct descendant we need to check the parent parent again + partIndex-- + } else { + // Otherwise we just move the base down + baseElementIndex-- + } + } + } + return wasPartMatched + } + + private checkElementAgainstSelectorPartRequirements(element: Element, requirements: Partial): boolean { + if (requirements.text && element.text !== requirements.text) { + return false + } + if (requirements.tag_name && element.tag_name !== requirements.tag_name) { + return false + } + if (requirements.href && element.href !== requirements.href) { + return false + } + if (requirements.attr_id && element.attr_id !== requirements.attr_id) { + return false + } + if (requirements.attr_class) { + if ( + !element.attr_class?.length || + !requirements.attr_class.every((className) => element.attr_class!.includes(className)) + ) { + return false + } + } + if (requirements.nth_child && element.nth_child !== requirements.nth_child) { + return false + } + if (requirements.nth_of_type && element.nth_of_type !== requirements.nth_of_type) { + return false + } + if (requirements.attributes) { + const { attributes } = element + if (!attributes) { + return false + } + for (const [key, value] of Object.entries(requirements.attributes)) { + if (attributes[key] !== value) { + return false + } + } + } + return true + } +} + +class SelectorPart { + directDescendant: boolean + uniqueOrder: number + requirements: Partial + + constructor(tag: string, directDescendant: boolean, escapeSlashes: boolean) { + const SELECTOR_ATTRIBUTE_REGEX = /([a-zA-Z]*)\[(.*)=[\'|\"](.*)[\'|\"]\]/ + this.directDescendant = directDescendant + this.uniqueOrder = 0 + this.requirements = {} + + const result = tag.match(SELECTOR_ATTRIBUTE_REGEX) + if (result && tag.includes('[id=')) { + this.requirements.attr_id = result[3] + tag = result[1] + } + if (result && tag.includes('[')) { + if (!this.requirements.attributes) { + this.requirements.attributes = {} + } + this.requirements.attributes[result[2]] = result[3] + tag = result[1] + } + if (tag.includes('nth-child(')) { + const nthChildParts = tag.split(':nth-child(') + this.requirements.nth_child = parseInt(nthChildParts[1].replace(')', '')) + tag = nthChildParts[0] + } + if (tag.includes('.')) { + const classParts = tag.split('.') + // Strip all slashes that are not followed by another slash + this.requirements.attr_class = classParts.slice(1) + if (escapeSlashes) { + this.requirements.attr_class = this.requirements.attr_class.map(this.unescapeClassName.bind(this)) + } // TODO: determine if we need escapeSlashes in this port + tag = classParts[0] + } + if (tag) { + this.requirements.tag_name = tag + } + } + + /** Separate all double slashes "\\" (replace them with "\") and remove all single slashes between them. */ + private unescapeClassName(className: string): string { + return className + .split('\\\\') + .map((p) => p.replace(/\\/g, '')) + .join('\\') + } +} diff --git a/src/worker/ingestion/ingest-event.ts b/src/worker/ingestion/ingest-event.ts index eacd9b72..5457eafd 100644 --- a/src/worker/ingestion/ingest-event.ts +++ b/src/worker/ingestion/ingest-event.ts @@ -6,14 +6,15 @@ import { Hub, IngestEventResponse } from '../../types' import { timeoutGuard } from '../../utils/db/utils' import { status } from '../../utils/status' -export async function ingestEvent(server: Hub, event: PluginEvent): Promise { +export async function ingestEvent(hub: Hub, event: PluginEvent): Promise { const timeout = timeoutGuard('Still ingesting event inside worker. Timeout warning after 30 sec!', { event: JSON.stringify(event), }) try { - const { distinct_id, ip, site_url, team_id, now, sent_at, uuid } = event - await server.eventsProcessor.processEvent( - distinct_id, + const { ip, site_url, team_id, now, sent_at, uuid } = event + const distinctId = event.distinct_id.toString() + const result = await hub.eventsProcessor.processEvent( + distinctId, ip, site_url, event, @@ -22,6 +23,13 @@ export async function ingestEvent(server: Hub, event: PluginEvent): Promise= 1 && result) { + const person = await hub.db.fetchPerson(team_id, distinctId) + const actionMatches = await hub.actionMatcher.match(event, person, result.elements) + if (hub.PLUGIN_SERVER_ACTION_MATCHING >= 2 && actionMatches.length && result.eventId !== undefined) { + await hub.db.registerEventActionOccurrences(result.eventId, actionMatches) + } + } // We don't want to return the inserted DB entry that `processEvent` returns. // This response is passed to piscina and would be discarded anyway. return { success: true } diff --git a/src/worker/ingestion/process-event.ts b/src/worker/ingestion/process-event.ts index 82f12ead..b8beffae 100644 --- a/src/worker/ingestion/process-event.ts +++ b/src/worker/ingestion/process-event.ts @@ -9,6 +9,7 @@ import { Event as EventProto, IEvent } from '../../config/idl/protos' import { KAFKA_EVENTS, KAFKA_SESSION_RECORDING_EVENTS } from '../../config/kafka-topics' import { Element, + Event, Hub, Person, PersonDistinctId, @@ -20,15 +21,25 @@ import { import { Client } from '../../utils/celery/client' import { DB } from '../../utils/db/db' import { KafkaProducerWrapper } from '../../utils/db/kafka-producer-wrapper' -import { elementsToString, personInitialAndUTMProperties, sanitizeEventName, timeoutGuard } from '../../utils/db/utils' +import { + elementsToString, + extractElements, + personInitialAndUTMProperties, + sanitizeEventName, + timeoutGuard, +} from '../../utils/db/utils' import { status } from '../../utils/status' import { castTimestampOrNow, filterIncrementProperties, UUID, UUIDT } from '../../utils/utils' -import { ActionManager } from './action-manager' import { PersonManager } from './person-manager' import { TeamManager } from './team-manager' +export interface EventProcessingResult { + event: IEvent | SessionRecordingEvent | PostgresSessionRecordingEvent + eventId?: number + elements?: Element[] +} + export class EventsProcessor { - ready: boolean pluginsServer: Hub db: DB clickhouse: ClickHouse | undefined @@ -36,10 +47,8 @@ export class EventsProcessor { celery: Client teamManager: TeamManager personManager: PersonManager - actionManager: ActionManager constructor(pluginsServer: Hub) { - this.ready = false this.pluginsServer = pluginsServer this.db = pluginsServer.db this.clickhouse = pluginsServer.clickhouse @@ -47,12 +56,6 @@ export class EventsProcessor { this.celery = new Client(pluginsServer.db, pluginsServer.CELERY_DEFAULT_QUEUE) this.teamManager = new TeamManager(pluginsServer.db) this.personManager = new PersonManager(pluginsServer) - this.actionManager = new ActionManager(pluginsServer.db) - } - - public async prepare(): Promise { - await this.actionManager.prepare() - this.ready = true } public async processEvent( @@ -64,10 +67,7 @@ export class EventsProcessor { now: DateTime, sentAt: DateTime | null, eventUuid: string - ): Promise { - if (!this.ready) { - throw new Error('EventsProcessor is not ready! Run eventsProcessor.prepare() before this') - } + ): Promise { if (!UUID.validateString(eventUuid, false)) { throw new Error(`Not a valid UUID: "${eventUuid}"`) } @@ -76,10 +76,9 @@ export class EventsProcessor { event: JSON.stringify(data), }) + let result: EventProcessingResult | void try { - // sanitize values, even though `sanitizeEvent` should have gotten to them - distinctId = distinctId?.toString() - + // Sanitize values, even though `sanitizeEvent` should have gotten to them const properties: Properties = data.properties ?? {} if (data['$set']) { properties['$set'] = { ...properties['$set'], ...data['$set'] } @@ -100,15 +99,13 @@ export class EventsProcessor { clearTimeout(timeout1) } - let result: IEvent | SessionRecordingEvent - if (data['event'] === '$snapshot') { const timeout2 = timeoutGuard( 'Still running "createSessionRecordingEvent". Timeout warning after 30 sec!', { eventUuid } ) try { - result = await this.createSessionRecordingEvent( + await this.createSessionRecordingEvent( eventUuid, teamId, distinctId, @@ -119,13 +116,14 @@ export class EventsProcessor { this.pluginsServer.statsd?.timing('kafka_queue.single_save.snapshot', singleSaveTimer, { team_id: teamId.toString(), }) + // No return value in case of snapshot events as we don't do action matching on them } finally { clearTimeout(timeout2) } } else { const timeout3 = timeoutGuard('Still running "capture". Timeout warning after 30 sec!', { eventUuid }) try { - result = await this.capture( + const [event, eventId, elements] = await this.capture( eventUuid, personUuid, ip, @@ -140,15 +138,19 @@ export class EventsProcessor { this.pluginsServer.statsd?.timing('kafka_queue.single_save.standard', singleSaveTimer, { team_id: teamId.toString(), }) + result = { + event, + eventId, + elements, + } } finally { clearTimeout(timeout3) } } - - return result } finally { clearTimeout(timeout) } + return result } private handleTimestamp(data: PluginEvent, now: DateTime, sentAt: DateTime | null): DateTime { @@ -375,22 +377,14 @@ export class EventsProcessor { properties: Properties, timestamp: DateTime, sentAt: DateTime | null - ): Promise { + ): Promise<[IEvent, Event['id'] | undefined, Element[] | undefined]> { event = sanitizeEventName(event) const elements: Record[] | undefined = properties['$elements'] let elementsList: Element[] = [] + if (elements && elements.length) { delete properties['$elements'] - elementsList = elements.map((el) => ({ - text: el['$el_text']?.slice(0, 400), - tag_name: el['tag_name'], - href: el['attr__href']?.slice(0, 2048), - attr_class: el['attr__class']?.split(' '), - attr_id: el['attr__id'], - nth_child: el['nth_child'], - nth_of_type: el['nth_of_type'], - attributes: Object.fromEntries(Object.entries(el).filter(([key]) => key.startsWith('attr__'))), - })) + elementsList = extractElements(elements) } const team = await this.teamManager.fetchTeam(teamId) @@ -449,14 +443,14 @@ export class EventsProcessor { timestamp?: DateTime | string, elements?: Element[], siteUrl?: string - ): Promise { + ): Promise<[IEvent, Event['id'] | undefined, Element[] | undefined]> { const timestampString = castTimestampOrNow( timestamp, this.kafkaProducer ? TimestampFormat.ClickHouse : TimestampFormat.ISO ) const elementsChain = elements && elements.length ? elementsToString(elements) : '' - const data: IEvent = { + const eventPayload: IEvent = { uuid, event, properties: JSON.stringify(properties ?? {}), @@ -467,13 +461,15 @@ export class EventsProcessor { createdAt: timestampString, } + let eventId: Event['id'] | undefined + if (this.kafkaProducer) { await this.kafkaProducer.queueMessage({ topic: KAFKA_EVENTS, messages: [ { key: uuid, - value: EventProto.encodeDelimited(EventProto.create(data)).finish() as Buffer, + value: EventProto.encodeDelimited(EventProto.create(eventPayload)).finish() as Buffer, }, ], }) @@ -505,24 +501,25 @@ export class EventsProcessor { } = await this.db.postgresQuery( 'INSERT INTO posthog_event (created_at, event, distinct_id, properties, team_id, timestamp, elements, elements_hash) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *', [ - data.createdAt, - data.event, + eventPayload.createdAt, + eventPayload.event, distinctId, - data.properties, - data.teamId, - data.timestamp, + eventPayload.properties, + eventPayload.teamId, + eventPayload.timestamp, JSON.stringify(elements || []), elementsHash, ], 'createEventInsert' ) + eventId = event.id if (await this.teamManager.shouldSendWebhooks(teamId)) { this.pluginsServer.statsd?.increment(`hooks.send_task`) this.celery.sendTask('posthog.tasks.webhooks.post_event_to_webhook', [event.id, siteUrl], {}) } } - return data + return [eventPayload, eventId, elements] } private async createSessionRecordingEvent( @@ -554,13 +551,14 @@ export class EventsProcessor { messages: [{ key: uuid, value: Buffer.from(JSON.stringify(data)) }], }) } else { - const insertResult = await this.db.postgresQuery( + const { + rows: [eventCreated], + } = await this.db.postgresQuery( 'INSERT INTO posthog_sessionrecordingevent (created_at, team_id, distinct_id, session_id, timestamp, snapshot_data) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *', [data.created_at, data.team_id, data.distinct_id, data.session_id, data.timestamp, data.snapshot_data], 'insertSessionRecording' ) - const eventCreated = insertResult.rows[0] as PostgresSessionRecordingEvent - return eventCreated + return eventCreated as PostgresSessionRecordingEvent } return data } diff --git a/src/worker/tasks.ts b/src/worker/tasks.ts index 30072c75..799889dc 100644 --- a/src/worker/tasks.ts +++ b/src/worker/tasks.ts @@ -43,13 +43,13 @@ export const workerTasks: Record = { await loadSchedule(hub) }, reloadAllActions: async (hub) => { - return await hub.eventsProcessor.actionManager.reloadAllActions() + return await hub.actionManager.reloadAllActions() }, reloadAction: async (hub, args: { teamId: Team['id']; actionId: Action['id'] }) => { - return await hub.eventsProcessor.actionManager.reloadAction(args.teamId, args.actionId) + return await hub.actionManager.reloadAction(args.teamId, args.actionId) }, dropAction: (hub, args: { teamId: Team['id']; actionId: Action['id'] }) => { - return hub.eventsProcessor.actionManager.dropAction(args.teamId, args.actionId) + return hub.actionManager.dropAction(args.teamId, args.actionId) }, teardownPlugins: async (hub) => { await teardownPlugins(hub) diff --git a/tests/helpers/sql.ts b/tests/helpers/sql.ts index 8a26ec7a..5cbfcbcf 100644 --- a/tests/helpers/sql.ts +++ b/tests/helpers/sql.ts @@ -76,7 +76,7 @@ export async function resetTestDatabase( await db.end() } -async function insertRow(db: Pool, table: string, object: Record): Promise { +export async function insertRow(db: Pool, table: string, object: Record): Promise { const keys = Object.keys(object) .map((key) => `"${key}"`) .join(',') diff --git a/tests/postgres/worker.test.ts b/tests/postgres/worker.test.ts index 2694f567..c0aa56ee 100644 --- a/tests/postgres/worker.test.ts +++ b/tests/postgres/worker.test.ts @@ -10,6 +10,7 @@ import { createHub } from '../../src/utils/db/hub' import { KafkaProducerWrapper } from '../../src/utils/db/kafka-producer-wrapper' import { delay, UUIDT } from '../../src/utils/utils' import { ActionManager } from '../../src/worker/ingestion/action-manager' +import { ActionMatcher } from '../../src/worker/ingestion/action-matcher' import { ingestEvent } from '../../src/worker/ingestion/ingest-event' import { makePiscina } from '../../src/worker/piscina' import { runPluginTask, runProcessEvent } from '../../src/worker/plugins/run' @@ -20,6 +21,7 @@ import { resetTestDatabase } from '../helpers/sql' import { setupPiscina } from '../helpers/worker' jest.mock('../../src/worker/ingestion/action-manager') +jest.mock('../../src/worker/ingestion/action-matcher') jest.mock('../../src/utils/db/sql') jest.mock('../../src/utils/status') jest.mock('../../src/worker/ingestion/ingest-event') @@ -45,6 +47,7 @@ beforeEach(() => { jest.spyOn(ActionManager.prototype, 'reloadAllActions') jest.spyOn(ActionManager.prototype, 'reloadAction') jest.spyOn(ActionManager.prototype, 'dropAction') + jest.spyOn(ActionMatcher.prototype, 'match') }) test('piscina worker test', async () => { @@ -180,7 +183,10 @@ describe('queue logic', () => { expect(pluginsServer.piscina.queueSize).toBe(0) const client = new Client(pluginsServer.hub.db, pluginsServer.hub.PLUGINS_CELERY_QUEUE) - for (let i = 0; i < 2; i++) { + + let tasksSentSoFar = 0 + + for (tasksSentSoFar; tasksSentSoFar < 2; tasksSentSoFar++) { client.sendTask('posthog.tasks.process_event.process_event_with_plugins', args, {}) } @@ -193,12 +199,13 @@ describe('queue logic', () => { await delay(5000) expect(pluginsServer.piscina.queueSize).toBe(0) - expect(pluginsServer.piscina.completed).toBe(baseCompleted + 2 * 3) // 2 x (process + on + ingest) + // tasksSentSoFar * (processEvent + onEvent + ingestEvent) + expect(pluginsServer.piscina.completed).toBe(baseCompleted + tasksSentSoFar * 3) expect(pluginsServer.queue.isPaused()).toBe(false) // 2 tasks * 2 threads = 4 active // 2 threads * 2 threads = 4 queue excess - for (let i = 0; i < 50; i++) { + for (tasksSentSoFar; tasksSentSoFar < 52; tasksSentSoFar++) { client.sendTask('posthog.tasks.process_event.process_event_with_plugins', args, {}) } @@ -220,7 +227,8 @@ describe('queue logic', () => { expect(pausedTimes).toBeGreaterThanOrEqual(10) expect(pluginsServer.queue.isPaused()).toBe(false) expect(pluginsServer.piscina.queueSize).toBe(0) - expect(pluginsServer.piscina.completed).toEqual(baseCompleted + 156) + // tasksSentSoFar x (processEvent + onEvent + ingestEvent) + expect(pluginsServer.piscina.completed).toEqual(baseCompleted + tasksSentSoFar * 3) const duration = pluginsServer.piscina.duration - startTime const expectedTimeMs = (50 / 4) * 1000 @@ -302,19 +310,19 @@ describe('createTaskRunner()', () => { it('handles `reloadAllActions` task', async () => { await taskRunner({ task: 'reloadAllActions' }) - expect(hub.eventsProcessor.actionManager.reloadAllActions).toHaveBeenCalledWith() + expect(hub.actionManager.reloadAllActions).toHaveBeenCalledWith() }) it('handles `reloadAction` task', async () => { await taskRunner({ task: 'reloadAction', args: { teamId: 2, actionId: 777 } }) - expect(hub.eventsProcessor.actionManager.reloadAction).toHaveBeenCalledWith(2, 777) + expect(hub.actionManager.reloadAction).toHaveBeenCalledWith(2, 777) }) it('handles `dropAction` task', async () => { await taskRunner({ task: 'dropAction', args: { teamId: 2, actionId: 777 } }) - expect(hub.eventsProcessor.actionManager.dropAction).toHaveBeenCalledWith(2, 777) + expect(hub.actionManager.dropAction).toHaveBeenCalledWith(2, 777) }) it('handles `teardownPlugin` task', async () => { diff --git a/tests/shared/process-event.ts b/tests/shared/process-event.ts index fe345523..e375f976 100644 --- a/tests/shared/process-event.ts +++ b/tests/shared/process-event.ts @@ -3,22 +3,12 @@ import * as IORedis from 'ioredis' import { DateTime } from 'luxon' import { performance } from 'perf_hooks' -import { IEvent } from '../../src/config/idl/protos' -import { - Database, - Event, - Hub, - LogLevel, - Person, - PluginsServerConfig, - SessionRecordingEvent, - Team, -} from '../../src/types' +import { Database, Event, Hub, LogLevel, Person, PluginsServerConfig, Team } from '../../src/types' import { createHub } from '../../src/utils/db/hub' import { hashElements } from '../../src/utils/db/utils' import { posthog } from '../../src/utils/posthog' import { delay, UUIDT } from '../../src/utils/utils' -import { EventsProcessor } from '../../src/worker/ingestion/process-event' +import { EventProcessingResult, EventsProcessor } from '../../src/worker/ingestion/process-event' import { createUserTeamAndOrganization, getFirstTeam, getTeams, onQuery, resetTestDatabase } from '../helpers/sql' jest.setTimeout(600000) // 600 sec timeout. @@ -100,7 +90,7 @@ export const createProcessEventTests = ( now: DateTime, sentAt: DateTime | null, eventUuid: string - ): Promise { + ): Promise { const response = await eventsProcessor.processEvent( distinctId, ip, @@ -129,7 +119,6 @@ export const createProcessEventTests = ( returned.hub = hub returned.closeHub = closeHub eventsProcessor = new EventsProcessor(hub) - await eventsProcessor.prepare() queryCounter = 0 processEventCounter = 0 team = await getFirstTeam(hub) @@ -1687,24 +1676,5 @@ export const createProcessEventTests = ( expect(person.properties).toEqual({ a: 1, b: 2, c: 3, d: 4 }) }) - test('distinct_id wrong type (number)', async () => { - await createPerson(hub, team, ['asdfasdfasdf']) - await processEvent( - (12345 as unknown) as string, - null, - '', - ({ - event: '$pageview', - properties: { distinct_id: 'asdfasdfasdf', token: team.api_token }, - } as any) as PluginEvent, - team.id, - now, - now, - new UUIDT().toString() - ) - const [event] = await hub.db.fetchEvents() - expect(event.distinct_id).toEqual('12345') - }) - return returned } diff --git a/tests/worker/ingestion/action-matcher.test.ts b/tests/worker/ingestion/action-matcher.test.ts new file mode 100644 index 00000000..ef012da8 --- /dev/null +++ b/tests/worker/ingestion/action-matcher.test.ts @@ -0,0 +1,325 @@ +import { PluginEvent } from '@posthog/plugin-scaffold/src/types' + +import { Action, ActionStep, ActionStepUrlMatching, Element, Hub, RawAction } from '../../../src/types' +import { createHub } from '../../../src/utils/db/hub' +import { ActionMatcher } from '../../../src/worker/ingestion/action-matcher' +import { commonUserId } from '../../helpers/plugins' +import { insertRow, resetTestDatabase } from '../../helpers/sql' + +describe('ActionMatcher', () => { + let hub: Hub + let closeServer: () => Promise + let actionMatcher: ActionMatcher + + beforeEach(async () => { + await resetTestDatabase() + ;[hub, closeServer] = await createHub() + actionMatcher = hub.actionMatcher + }) + + afterEach(async () => { + await closeServer() + }) + + /** Return a test action created on a common base using provided steps. */ + async function createTestAction(partialSteps: Partial[]): Promise { + const action: RawAction = { + id: 1, + team_id: 2, + name: 'Test', + created_at: new Date().toISOString(), + created_by_id: commonUserId, + deleted: false, + post_to_slack: false, + slack_message_format: '', + is_calculating: false, + updated_at: new Date().toISOString(), + last_calculated_at: new Date().toISOString(), + } + const steps: ActionStep[] = partialSteps.map( + (partialStep, index) => + ({ + id: index + 50, + action_id: action.id, + tag_name: null, + text: null, + href: null, + selector: null, + url: null, + url_matching: null, + name: null, + event: null, + properties: null, + ...partialStep, + } as ActionStep) + ) + await insertRow(hub.db.postgres, 'posthog_action', action) + await Promise.all(steps.map((step) => insertRow(hub.db.postgres, 'posthog_actionstep', step))) + await hub.actionManager.reloadAction(action.team_id, action.id) + return { ...action, steps } + } + + /** Return a test event created on a common base using provided property overrides. */ + function createTestEvent(overrides: Partial = {}): PluginEvent { + const url: string = overrides.properties?.$current_url ?? 'http://example.com/foo/' + return { + distinct_id: 'my_id', + ip: '127.0.0.1', + site_url: url, + team_id: 2, + now: new Date().toISOString(), + event: '$pageview', + properties: { $current_url: url }, + ...overrides, + } + } + + describe('#match()', () => { + it('returns no match if action has no steps', async () => { + const actionDefinition: Action = await createTestAction([]) + + const event: PluginEvent = createTestEvent() + + expect(await actionMatcher.match(event)).toEqual([]) + }) + + it('returns a match in case of URL contains page view', async () => { + const actionDefinition: Action = await createTestAction([ + { + url: 'example.com', + url_matching: ActionStepUrlMatching.Contains, + event: '$pageview', + }, + ]) + + const eventPosthog: PluginEvent = createTestEvent({ + properties: { $current_url: 'http://posthog.com/pricing' }, + }) + const eventExample: PluginEvent = createTestEvent({ + properties: { $current_url: 'https://example.com/' }, + }) + + expect(await actionMatcher.match(eventPosthog)).toEqual([]) + expect(await actionMatcher.match(eventExample)).toEqual([actionDefinition]) + }) + + it('returns a match in case of URL contains page views with % and _', async () => { + const actionDefinition: Action = await createTestAction([ + { + url: 'exampl_.com/%.html', + url_matching: ActionStepUrlMatching.Contains, + event: '$pageview', + }, + ]) + + const eventExample: PluginEvent = createTestEvent({ + properties: { $current_url: 'https://example.com/' }, + }) + const eventExampleHtml: PluginEvent = createTestEvent({ + properties: { $current_url: 'https://example.com/index.html' }, + }) + + expect(await actionMatcher.match(eventExample)).toEqual([]) + expect(await actionMatcher.match(eventExampleHtml)).toEqual([actionDefinition]) + }) + + it('returns a match in case of URL matches regex page views', async () => { + const actionDefinition: Action = await createTestAction([ + { + url: String.raw`^https?:\/\/example\.com\/\d+(\/[a-r]*\/?)?$`, + url_matching: ActionStepUrlMatching.Regex, + event: '$pageview', + }, + ]) + + const eventExampleOk1: PluginEvent = createTestEvent({ + properties: { $current_url: 'https://example.com/23/hello' }, + }) + const eventExampleOk2: PluginEvent = createTestEvent({ + properties: { $current_url: 'https://example.com/3/abc/' }, + }) + const eventExampleBad1: PluginEvent = createTestEvent({ + properties: { $current_url: 'https://example.com/3/xyz/' }, + }) + const eventExampleBad2: PluginEvent = createTestEvent({ + properties: { $current_url: 'https://example.com/' }, + }) + const eventExampleBad3: PluginEvent = createTestEvent({ + properties: { $current_url: 'https://example.com/uno/dos/' }, + }) + const eventExampleBad4: PluginEvent = createTestEvent({ + properties: { $current_url: 'https://example.com/1foo/' }, + }) + + expect(await actionMatcher.match(eventExampleOk1)).toEqual([actionDefinition]) + expect(await actionMatcher.match(eventExampleOk2)).toEqual([actionDefinition]) + expect(await actionMatcher.match(eventExampleBad1)).toEqual([]) + expect(await actionMatcher.match(eventExampleBad2)).toEqual([]) + expect(await actionMatcher.match(eventExampleBad3)).toEqual([]) + expect(await actionMatcher.match(eventExampleBad4)).toEqual([]) + }) + + it('returns a match in case of URL matches exactly page views', async () => { + const actionDefinition: Action = await createTestAction([ + { + url: 'https://www.mozilla.org/de/', + url_matching: ActionStepUrlMatching.Exact, + event: '$pageview', + }, + ]) + + const eventExampleOk: PluginEvent = createTestEvent({ + properties: { $current_url: 'https://www.mozilla.org/de/' }, + }) + const eventExampleBad1: PluginEvent = createTestEvent({ + properties: { $current_url: 'https://www.mozilla.org/de' }, + }) + const eventExampleBad2: PluginEvent = createTestEvent({ + properties: { $current_url: 'https://www.mozilla.org/de/firefox/' }, + }) + + expect(await actionMatcher.match(eventExampleOk)).toEqual([actionDefinition]) + expect(await actionMatcher.match(eventExampleBad1)).toEqual([]) + expect(await actionMatcher.match(eventExampleBad2)).toEqual([]) + }) + + it('returns a match in case of exact event name', async () => { + const actionDefinition: Action = await createTestAction([ + { + event: 'meow', + }, + ]) + + const eventExampleOk: PluginEvent = createTestEvent({ + event: 'meow', + }) + const eventExampleBad1: PluginEvent = createTestEvent({ + event: '$meow', + }) + const eventExampleBad2: PluginEvent = createTestEvent({ + event: 'WOOF', + }) + + expect(await actionMatcher.match(eventExampleOk)).toEqual([actionDefinition]) + expect(await actionMatcher.match(eventExampleBad1)).toEqual([]) + expect(await actionMatcher.match(eventExampleBad2)).toEqual([]) + }) + + it('returns a match in case of exact event name AND URL contains', async () => { + const actionDefinition: Action = await createTestAction([ + { + event: 'meow', + url_matching: ActionStepUrlMatching.Contains, + url: 'pets.com/', + }, + ]) + + const eventExampleOk1: PluginEvent = createTestEvent({ + event: 'meow', + properties: { $current_url: 'http://www.pets.com/' }, + }) + const eventExampleOk2: PluginEvent = createTestEvent({ + event: 'meow', + properties: { $current_url: 'https://pets.com/food' }, + }) + const eventExampleBad1: PluginEvent = createTestEvent({ + event: '$meow', + properties: { $current_url: 'https://xyz.pets.com/' }, + }) + const eventExampleBad2: PluginEvent = createTestEvent({ + event: 'meow', + properties: { $current_url: 'https://www.pets.com.de/' }, + }) + const eventExampleBad3: PluginEvent = createTestEvent({ + event: 'meow', + properties: { $current_url: 'https://www.pets.co' }, + }) + + expect(await actionMatcher.match(eventExampleOk1)).toEqual([actionDefinition]) + expect(await actionMatcher.match(eventExampleOk2)).toEqual([actionDefinition]) + expect(await actionMatcher.match(eventExampleBad1)).toEqual([]) + expect(await actionMatcher.match(eventExampleBad2)).toEqual([]) + expect(await actionMatcher.match(eventExampleBad3)).toEqual([]) + }) + }) + + describe('#checkElementsAgainstSelector()', () => { + it('handles any descendant selector', () => { + const elements: Element[] = [ + { tag_name: 'h1', attr_class: ['headline'] }, + { tag_name: 'div', attr_class: ['top'] }, + { tag_name: 'main' }, + ] + + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main h1')).toBeTruthy() + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main .headline')).toBeTruthy() + + expect(actionMatcher.checkElementsAgainstSelector(elements, 'h1 div')).toBeFalsy() + expect(actionMatcher.checkElementsAgainstSelector(elements, '.top main')).toBeFalsy() + + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main div')).toBeTruthy() + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main .top')).toBeTruthy() + expect(actionMatcher.checkElementsAgainstSelector(elements, 'div h1')).toBeTruthy() + expect(actionMatcher.checkElementsAgainstSelector(elements, 'div .headline')).toBeTruthy() + }) + + it('handles direct descendant selector', () => { + const elements: Element[] = [ + { tag_name: 'h1', attr_class: ['headline'] }, + { tag_name: 'div', attr_class: ['top'] }, + { tag_name: 'main' }, + ] + + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main > h1')).toBeFalsy() + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main > .headline')).toBeFalsy() + + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main > .top > h1')).toBeTruthy() + + expect(actionMatcher.checkElementsAgainstSelector(elements, 'h1 > div')).toBeFalsy() + expect(actionMatcher.checkElementsAgainstSelector(elements, '.top > main')).toBeFalsy() + + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main > div')).toBeTruthy() + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main > .top')).toBeTruthy() + expect(actionMatcher.checkElementsAgainstSelector(elements, 'div > h1')).toBeTruthy() + expect(actionMatcher.checkElementsAgainstSelector(elements, 'div > .headline')).toBeTruthy() + }) + + it('handles direct descendant selector edge cases 1', () => { + const elements: Element[] = [ + { tag_name: 'h1', attr_class: ['headline'] }, + { tag_name: 'div', attr_class: ['inner'] }, + { tag_name: 'div', attr_class: ['outer'] }, + { tag_name: 'main' }, + ] + + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main > h1')).toBeFalsy() + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main > .inner')).toBeFalsy() + + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main > .outer > .inner > h1')).toBeTruthy() + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main > .inner > h1')).toBeFalsy() + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main > .outer > h1')).toBeFalsy() + + expect(actionMatcher.checkElementsAgainstSelector(elements, 'h1 > div')).toBeFalsy() + expect(actionMatcher.checkElementsAgainstSelector(elements, 'outer > main')).toBeFalsy() + + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main > div')).toBeTruthy() + expect(actionMatcher.checkElementsAgainstSelector(elements, 'main > .outer')).toBeTruthy() + expect(actionMatcher.checkElementsAgainstSelector(elements, 'div > h1')).toBeTruthy() + expect(actionMatcher.checkElementsAgainstSelector(elements, '.inner > .headline')).toBeTruthy() + }) + + it('handles direct descendant selector edge cases 2', () => { + const elements: Element[] = [ + { tag_name: 'span' }, + { tag_name: 'div' }, + { tag_name: 'a' }, + { tag_name: 'div' }, + { tag_name: 'div' }, + { tag_name: 'aside' }, + { tag_name: 'section' }, + ] + + expect(actionMatcher.checkElementsAgainstSelector(elements, 'aside div > span')).toBeTruthy() + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index edf4ebd4..4c7a6341 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1446,6 +1446,14 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" +"@npmcli/move-file@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + "@posthog/clickhouse@^1.7.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@posthog/clickhouse/-/clickhouse-1.7.0.tgz#21fa1e8cfa0637b688f91964e0efeedbf4cf7a3c" @@ -1972,6 +1980,11 @@ abab@^2.0.3: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -2029,6 +2042,15 @@ agent-base@^4.3.0: dependencies: es6-promisify "^5.0.0" +agentkeepalive@^4.1.3: + version "4.1.4" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.1.4.tgz#d928028a4862cb11718e55227872e842a44c945b" + integrity sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ== + dependencies: + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -2069,6 +2091,16 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: dependencies: type-fest "^0.11.0" +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + ansi-regex@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" @@ -2104,6 +2136,19 @@ anymatch@^3.0.3, anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -2617,6 +2662,29 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +cacache@^15.0.5: + version "15.2.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.2.0.tgz#73af75f77c58e72d8c630a7a2858cb18ef523389" + integrity sha512-uKoJSHmnrqXgthDFx/IU6ED/5xd+NNGe+Bb+kLZy7Ku4P+BaiWEUflAKPZ7eAzsYGcsAGASJZsybXp+quEcHTw== + dependencies: + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" + unique-filename "^1.1.1" + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -2759,6 +2827,11 @@ chokidar@^3.5.1: optionalDependencies: fsevents "~2.3.1" +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + ci-info@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" @@ -2827,6 +2900,11 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" @@ -2930,6 +3008,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -3142,11 +3225,21 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + denque@^1.1.0: version "1.4.1" resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== +depd@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -3247,6 +3340,13 @@ enabled@2.0.x: resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== +encoding@^0.1.12: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -3266,6 +3366,16 @@ ent@^2.2.0: resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -3888,6 +3998,13 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs-readdir-recursive@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" @@ -3923,6 +4040,20 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + gaxios@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.1.0.tgz#e8ad466db5a4383c70b9d63bfd14dfaa87eb0099" @@ -4106,6 +4237,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.2.4: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +graceful-fs@^4.2.6: + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== + graphile-worker@^0.11.1: version "0.11.1" resolved "https://registry.yarnpkg.com/graphile-worker/-/graphile-worker-0.11.1.tgz#770e0144672b9eaa15d7854ebe90448c8da0b260" @@ -4174,6 +4310,11 @@ has-symbols@^1.0.2: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -4250,7 +4391,12 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -http-proxy-agent@^4.0.0: +http-cache-semantics@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + +http-proxy-agent@^4.0.0, http-proxy-agent@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== @@ -4289,6 +4435,13 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0= + dependencies: + ms "^2.0.0" + husky@>=4: version "4.3.5" resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.5.tgz#ab8d2a0eb6b62fef2853ee3d442c927d89290902" @@ -4312,6 +4465,13 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ieee754@1.1.13: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" @@ -4365,6 +4525,11 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -4378,6 +4543,11 @@ inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +install-artifact-from-github@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.2.0.tgz#adcbd123c16a4337ec44ea76d0ebf253cc16b074" + integrity sha512-3OxCPcY55XlVM3kkfIpeCgmoSKnMsz2A3Dbhsq0RXpIknKQmrX1YiznCeW9cD2ItFmDxziA3w6Eg8d80AoL3oA== + internal-slot@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" @@ -4416,6 +4586,11 @@ ip6addr@^0.2.3: assert-plus "^1.0.0" jsprim "^1.4.0" +ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + is-accessor-descriptor@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" @@ -4566,6 +4741,18 @@ is-finite@^1.0.0: resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -4590,6 +4777,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= + is-nan@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03" @@ -5665,6 +5857,27 @@ make-error@1.x, make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +make-fetch-happen@^8.0.14: + version "8.0.14" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz#aaba73ae0ab5586ad8eaa68bd83332669393e222" + integrity sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.0.5" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + promise-retry "^2.0.1" + socks-proxy-agent "^5.0.0" + ssri "^8.0.0" + makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -5804,6 +6017,60 @@ minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-fetch@^1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" + integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + +minizlib@^2.0.0, minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -5859,12 +6126,12 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1: +ms@^2.0.0, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nan@^2.13.2: +nan@^2.13.2, nan@^2.14.2: version "2.14.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== @@ -5924,6 +6191,22 @@ node-gyp-build@^4.2.2: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739" integrity sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg== +node-gyp@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.1.0.tgz#81f43283e922d285c886fb0e0f520a7fd431d8c2" + integrity sha512-o2elh1qt7YUp3lkMwY3/l4KF3j/A3fI/Qt4NH+CQQgPJdqGE9y7qnP84cjIWN27Q0jJkrSAhCVDg+wBVNBYdBg== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^8.0.14" + nopt "^5.0.0" + npmlog "^4.1.2" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.0" + which "^2.0.2" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -5965,6 +6248,13 @@ node-schedule@^1.3.2: long-timeout "0.1.1" sorted-array-functions "^1.0.0" +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -6001,6 +6291,21 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.0.0" +npmlog@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + nwsapi@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" @@ -6011,7 +6316,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.0.1, object-assign@^4.1.1: +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -6573,6 +6878,19 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + prompts@^2.0.1: version "2.4.0" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" @@ -6652,6 +6970,15 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +re2@^1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/re2/-/re2-1.16.0.tgz#f311eb4865b1296123800ea8e013cec8dab25590" + integrity sha512-eizTZL2ZO0ZseLqfD4t3Qd0M3b3Nr0MBWpX81EbPMIud/1d/CSfUIx2GQK8fWiAeHoSekO5EOeFib2udTZLwYw== + dependencies: + install-artifact-from-github "^1.2.0" + nan "^2.14.2" + node-gyp "^8.0.0" + react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -6715,7 +7042,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^2.0.2, readable-stream@^2.3.7: +readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.3.7: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -7008,6 +7335,11 @@ retry-request@^4.1.1: dependencies: debug "^4.1.1" +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -7061,7 +7393,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -7142,7 +7474,7 @@ semver@~5.0.1: resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" integrity sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no= -set-blocking@^2.0.0: +set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= @@ -7245,6 +7577,11 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" +smart-buffer@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba" + integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw== + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -7304,6 +7641,23 @@ snowflake-sdk@^1.6.0: uuid "^3.3.2" winston "^3.1.0" +socks-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-5.0.0.tgz#7c0f364e7b1cf4a7a437e71253bed72e9004be60" + integrity sha512-lEpa1zsWCChxiynk+lCycKuC502RxDWLKJZoIhnxrWNjLSDGYRFflHA1/228VkRcnv9TIb8w98derGbpKxJRgA== + dependencies: + agent-base "6" + debug "4" + socks "^2.3.3" + +socks@^2.3.3: + version "2.6.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e" + integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA== + dependencies: + ip "^1.1.5" + smart-buffer "^4.1.0" + sorted-array-functions@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz#8605695563294dffb2c9796d602bd8459f7a0dd5" @@ -7408,6 +7762,13 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -7463,6 +7824,23 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + string-width@^4.1.0, string-width@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" @@ -7541,6 +7919,20 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + strip-ansi@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" @@ -7672,6 +8064,18 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" +tar@^6.0.2, tar@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" + integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + teeny-request@^7.0.0: version "7.0.1" resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-7.0.1.tgz#bdd41fdffea5f8fbc0d29392cb47bec4f66b2b4c" @@ -8010,6 +8414,20 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + unix-dgram@2.0.x: version "2.0.4" resolved "https://registry.yarnpkg.com/unix-dgram/-/unix-dgram-2.0.4.tgz#14d4fc21e539742b8fb027de16eccd4e5503a344" @@ -8204,6 +8622,13 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + winston-transport@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59"