From d89ac78aa728f6fe61a57bdfe0b31b91d43f6779 Mon Sep 17 00:00:00 2001 From: Tiina Turban Date: Thu, 5 Oct 2023 17:16:04 +0200 Subject: [PATCH 01/11] feat: Create elements chain string as we store it --- src/autocapture-utils.ts | 88 +++++++++++++++++++++++++++++++++++++++- src/autocapture.ts | 3 +- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/autocapture-utils.ts b/src/autocapture-utils.ts index 8a6f219a7..98c682c67 100644 --- a/src/autocapture-utils.ts +++ b/src/autocapture-utils.ts @@ -3,7 +3,7 @@ * @param {Element} el - element to get the className of * @returns {string} the element's class */ -import { AutocaptureConfig } from 'types' +import { AutocaptureConfig, Properties } from 'types' import { _each, _includes, _isUndefined, _trim } from './utils' export function getClassName(el: Element): string { @@ -342,3 +342,89 @@ export function getNestedSpanText(target: Element): string { } return text } + +/* +Back in the day storing events in Postgres we use Elements for autocapture events. +Now we're using elements_chain. We used to do this parsing/processing during ingestion. +This code is just copied over from ingestion, but we should optimize it +to create elements_chain string directly. +*/ +export function getElementsChainString(elements: Properties[]): string { + return elementsToString(extractElements(elements)) +} + +interface Element { + text?: string; + tag_name?: string; + href?: string; + attr_id?: string; + attr_class?: string[]; + nth_child?: number; + nth_of_type?: number; + attributes?: Record; + event_id?: number; + order?: number; + group_id?: number; +} + +function escapeQuotes(input: string): string { + return input.replace(/"/g, '\\"') +} + +function elementsToString(elements: Element[]): string { + const ret = elements.map((element) => { + let el_string = '' + if (element.tag_name) { + el_string += element.tag_name + } + if (element.attr_class) { + element.attr_class.sort() + for (const single_class of element.attr_class) { + el_string += `.${single_class.replace(/"/g, '')}` + } + } + let attributes: Record = { + ...(element.text ? { text: element.text } : {}), + 'nth-child': element.nth_child ?? 0, + 'nth-of-type': element.nth_of_type ?? 0, + ...(element.href ? { href: element.href } : {}), + ...(element.attr_id ? { attr_id: element.attr_id } : {}), + ...element.attributes, + } + attributes = Object.fromEntries( + Object.entries(attributes) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, value]) => [escapeQuotes(key.toString()), escapeQuotes(value.toString())]) + ) + el_string += ':' + el_string += Object.entries(attributes) + .map(([key, value]) => `${key}="${value}"`) + .join('') + return el_string + }) + return ret.join(';') +} + +function extractElements(elements: Properties[]): 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: extractAttrClass(el), + 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__'))), + })) +} + +function extractAttrClass(el: Properties): Element['attr_class'] { + const attr_class = el['attr__class'] + if (!attr_class) { + return undefined + } else if (Array.isArray(attr_class)) { + return attr_class + } else { + return attr_class.split(' ') + } +} diff --git a/src/autocapture.ts b/src/autocapture.ts index 09941d3ce..53fc5f669 100644 --- a/src/autocapture.ts +++ b/src/autocapture.ts @@ -23,6 +23,7 @@ import { isAngularStyleAttr, isDocumentFragment, getDirectAndNestedSpanText, + getElementsChainString, } from './autocapture-utils' import RageClick from './extensions/rageclick' import { AutocaptureConfig, AutoCaptureCustomProperty, DecideResponse, Properties } from './types' @@ -263,7 +264,7 @@ const autocapture = { const props = _extend( this._getDefaultProperties(e.type), { - $elements: elementsJson, + $elements_chain: getElementsChainString(elementsJson), }, elementsJson[0]?.['$el_text'] ? { $el_text: elementsJson[0]?.['$el_text'] } : {}, this._getCustomProperties(targetElementList), From 80f2551a82155a503c16f37e513d22ecdd02a1aa Mon Sep 17 00:00:00 2001 From: Dave Murphy Date: Tue, 7 Nov 2023 08:54:45 -0800 Subject: [PATCH 02/11] Rename back-end Element interface to be PHElement so it doesn't conflict when run in the browser --- src/autocapture-utils.ts | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/autocapture-utils.ts b/src/autocapture-utils.ts index 4d16fba90..40cfe761c 100644 --- a/src/autocapture-utils.ts +++ b/src/autocapture-utils.ts @@ -4,10 +4,10 @@ * @returns {string} the element's class */ -import { AutocaptureConfig } from 'types' +import { AutocaptureConfig, Properties } from 'types' import { _each, _includes, _trim } from './utils' -import { _isNull, _isString, _isUndefined } from './utils/type-utils' +import { _isArray, _isNull, _isString, _isUndefined } from './utils/type-utils' import { logger } from './utils/logger' export function getClassName(el: Element): string { @@ -357,25 +357,27 @@ export function getElementsChainString(elements: Properties[]): string { return elementsToString(extractElements(elements)) } -interface Element { - text?: string; - tag_name?: string; - href?: string; - attr_id?: string; - attr_class?: string[]; - nth_child?: number; - nth_of_type?: number; - attributes?: Record; - event_id?: number; - order?: number; - group_id?: number; +// This interface is called 'Element' in plugin-scaffold https://github.com/PostHog/plugin-scaffold/blob/b07d3b879796ecc7e22deb71bf627694ba05386b/src/types.ts#L200 +// However 'Element' is a DOM Element when run in the browser, so we have to rename it +interface PHElement { + text?: string + tag_name?: string + href?: string + attr_id?: string + attr_class?: string[] + nth_child?: number + nth_of_type?: number + attributes?: Record + event_id?: number + order?: number + group_id?: number } function escapeQuotes(input: string): string { return input.replace(/"/g, '\\"') } -function elementsToString(elements: Element[]): string { +function elementsToString(elements: PHElement[]): string { const ret = elements.map((element) => { let el_string = '' if (element.tag_name) { @@ -409,7 +411,7 @@ function elementsToString(elements: Element[]): string { return ret.join(';') } -function extractElements(elements: Properties[]): Element[] { +function extractElements(elements: Properties[]): PHElement[] { return elements.map((el) => ({ text: el['$el_text']?.slice(0, 400), tag_name: el['tag_name'], @@ -422,11 +424,11 @@ function extractElements(elements: Properties[]): Element[] { })) } -function extractAttrClass(el: Properties): Element['attr_class'] { +function extractAttrClass(el: Properties): PHElement['attr_class'] { const attr_class = el['attr__class'] if (!attr_class) { return undefined - } else if (Array.isArray(attr_class)) { + } else if (_isArray(attr_class)) { return attr_class } else { return attr_class.split(' ') From b279c3a4b1d0f2a2e7572b42ae5c176a042854db Mon Sep 17 00:00:00 2001 From: Dave Murphy Date: Tue, 7 Nov 2023 09:14:11 -0800 Subject: [PATCH 03/11] Add tests for populated and unpopulated arrays --- src/__tests__/autocapture-utils.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/__tests__/autocapture-utils.js b/src/__tests__/autocapture-utils.js index 142a9ef20..a540c7c63 100644 --- a/src/__tests__/autocapture-utils.js +++ b/src/__tests__/autocapture-utils.js @@ -9,6 +9,7 @@ import { isAngularStyleAttr, getNestedSpanText, getDirectAndNestedSpanText, + getElementsChainString, } from '../autocapture-utils' describe(`Autocapture utility functions`, () => { @@ -397,4 +398,19 @@ describe(`Autocapture utility functions`, () => { expect(getNestedSpanText(parent)).toBe('test test2') }) }) + + describe('getElementsChainString', () => { + it('should return an empty string with no elements', () => { + const elementChain = getElementsChainString([]) + + expect(elementChain).toEqual('') + }) + it('should process elements correctly', () => { + const elementChain = getElementsChainString([ + { tag_name: 'div', nth_child: 1, nth_of_type: 2, $el_text: 'text' }, + ]) + + expect(elementChain).toEqual('div:nth-child="1"nth-of-type="2"text="text"') + }) + }) }) From f0efcbca0578ba204e9609e20af52ff6fb7c3f3f Mon Sep 17 00:00:00 2001 From: Dave Murphy Date: Tue, 7 Nov 2023 10:22:18 -0800 Subject: [PATCH 04/11] Change all tests that use $elements to use $elements_chain instead --- src/__tests__/autocapture.js | 38 ++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/__tests__/autocapture.js b/src/__tests__/autocapture.js index 5e5d6c9b9..ea68e5852 100644 --- a/src/__tests__/autocapture.js +++ b/src/__tests__/autocapture.js @@ -706,10 +706,10 @@ describe('Autocapture system', () => { const props = captureArgs[1] expect(event).toBe('$autocapture') expect(props['$event_type']).toBe('click') - expect(props['$elements'][0]).toHaveProperty('attr__href', 'http://test.com') - expect(props['$elements'][1]).toHaveProperty('tag_name', 'span') - expect(props['$elements'][2]).toHaveProperty('tag_name', 'div') - expect(props['$elements'][props['$elements'].length - 1]).toHaveProperty('tag_name', 'body') + expect(props['$elements_chain']).toContain('attr__href="http://test.com"') + expect(props['$elements_chain']).toContain('span:') + expect(props['$elements_chain']).toContain('div:') + expect(props['$elements_chain']).toContain('body:') }) it('truncate any element property value to 1024 bytes', () => { @@ -733,7 +733,7 @@ describe('Autocapture system', () => { const captureArgs = lib.capture.args[0] const props = captureArgs[1] expect(longString).toBe('prop'.repeat(400)) - expect(props['$elements'][0]).toHaveProperty('attr__data-props', 'prop'.repeat(256) + '...') + expect(props['$elements_chain']).toContain('a:attr__data-props="' + 'prop'.repeat(256) + '..."') }) it('gets the href attribute from parent anchor tags', () => { @@ -750,7 +750,7 @@ describe('Autocapture system', () => { }, lib ) - expect(getCapturedProps(lib.capture)['$elements'][0]).toHaveProperty('attr__href', 'http://test.com') + expect(getCapturedProps(lib.capture)['$elements_chain']).toContain('a:attr__href="http://test.com"') }) it('does not capture href attribute values from password elements', () => { @@ -784,7 +784,7 @@ describe('Autocapture system', () => { }, lib ) - expect(getCapturedProps(lib.capture)['$elements'][0]).not.toHaveProperty('attr__href') + expect(getCapturedProps(lib.capture)['$elements_chain']).not.toContain('a:attr__href') }) it('does not capture href attribute values that look like credit card numbers', () => { @@ -801,7 +801,7 @@ describe('Autocapture system', () => { }, lib ) - expect(getCapturedProps(lib.capture)['$elements'][0]).not.toHaveProperty('attr__href') + expect(getCapturedProps(lib.capture)['$elements_chain']).not.toContain('a:attr__href') }) it('does not capture href attribute values that look like social-security numbers', () => { @@ -818,7 +818,7 @@ describe('Autocapture system', () => { }, lib ) - expect(getCapturedProps(lib.capture)['$elements'][0]).not.toHaveProperty('attr__href') + expect(getCapturedProps(lib.capture)['$elements_chain']).not.toContain('a:attr__href') }) it('correctly identifies and formats text content', () => { @@ -866,7 +866,7 @@ describe('Autocapture system', () => { const props1 = getCapturedProps(lib.capture) const text1 = "Some super duper really long Text with new lines that we'll strip out and also we will want to make this text shorter since it's not likely people really care about text content that's super long and it also takes up more space and bandwidth. Some super d" - expect(props1['$elements'][0]).toHaveProperty('$el_text', text1) + expect(props1['$elements_chain']).toContain(`text="${text1}"`) expect(props1['$el_text']).toEqual(text1) lib.capture.resetHistory() @@ -876,7 +876,7 @@ describe('Autocapture system', () => { } autocapture._captureEvent(e2, lib) const props2 = getCapturedProps(lib.capture) - expect(props2['$elements'][0]).toHaveProperty('$el_text', 'Some text') + expect(props2['$elements_chain']).toContain('text="Some text"') expect(props2['$el_text']).toEqual('Some text') lib.capture.resetHistory() @@ -886,8 +886,7 @@ describe('Autocapture system', () => { } autocapture._captureEvent(e3, lib) const props3 = getCapturedProps(lib.capture) - expect(props3['$elements'][0]).toHaveProperty('$el_text', '') - expect(props3).not.toHaveProperty('$el_text') + expect(props3['$elements_chain']).not.toContain('text=""') }) it('does not capture sensitive text content', () => { @@ -916,8 +915,7 @@ describe('Autocapture system', () => { } autocapture._captureEvent(e1, lib) const props1 = getCapturedProps(lib.capture) - expect(props1['$elements'][0]).toHaveProperty('$el_text') - expect(props1['$elements'][0]['$el_text']).toMatch(/Why\s+hello\s+there/) + expect(props1['$elements_chain']).toContain('text="Why hello there"') lib.capture.resetHistory() const e2 = { @@ -926,8 +924,7 @@ describe('Autocapture system', () => { } autocapture._captureEvent(e2, lib) const props2 = getCapturedProps(lib.capture) - expect(props2['$elements'][0]).toHaveProperty('$el_text') - expect(props2['$elements'][0]['$el_text']).toMatch(/Why\s+hello\s+there/) + expect(props2['$elements_chain']).toContain('text="Why hello there"') lib.capture.resetHistory() const e3 = { @@ -936,8 +933,7 @@ describe('Autocapture system', () => { } autocapture._captureEvent(e3, lib) const props3 = getCapturedProps(lib.capture) - expect(props3['$elements'][0]).toHaveProperty('$el_text') - expect(props3['$elements'][0]['$el_text']).toMatch(/Why\s+hello\s+there/) + expect(props3['$elements_chain']).toContain('text="Why hello there"') }) it('should capture a submit event with form field props', () => { @@ -1031,7 +1027,7 @@ describe('Autocapture system', () => { autocapture._captureEvent(e1, newLib) const props1 = getCapturedProps(newLib.capture) - expect('attr__formmethod' in props1['$elements'][0]).toEqual(false) + expect(props1['$elements_chain']).not.toContain('attr__formmethod') }) it('does not capture any textContent if mask_all_text is set', () => { @@ -1060,7 +1056,7 @@ describe('Autocapture system', () => { autocapture._captureEvent(e1, newLib) const props1 = getCapturedProps(newLib.capture) - expect(props1['$elements'][0]).not.toHaveProperty('$el_text') + expect(props1['$elements_chain']).not.toHaveProperty('text') }) }) From f79e44bce26609ec1397960cb11a3beb36ec2617 Mon Sep 17 00:00:00 2001 From: Dave Murphy Date: Tue, 7 Nov 2023 11:34:40 -0800 Subject: [PATCH 05/11] Esacape already escaped double quotes per https://github.com/PostHog/posthog-js/security/code-scanning/16 --- src/autocapture-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocapture-utils.ts b/src/autocapture-utils.ts index 40cfe761c..c654e8f8a 100644 --- a/src/autocapture-utils.ts +++ b/src/autocapture-utils.ts @@ -374,7 +374,7 @@ interface PHElement { } function escapeQuotes(input: string): string { - return input.replace(/"/g, '\\"') + return input.replace(/"|\\"/g, '\\"') } function elementsToString(elements: PHElement[]): string { From 6453f8f7250468219249f22c730f3f07a554295b Mon Sep 17 00:00:00 2001 From: Dave Murphy Date: Tue, 14 Nov 2023 10:51:29 -0800 Subject: [PATCH 06/11] Remove `Object.entries` and `Object.fromEntries` as they are not supported in IE11 --- src/autocapture-utils.ts | 43 +++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/autocapture-utils.ts b/src/autocapture-utils.ts index c654e8f8a..4043d8386 100644 --- a/src/autocapture-utils.ts +++ b/src/autocapture-utils.ts @@ -5,7 +5,7 @@ */ import { AutocaptureConfig, Properties } from 'types' -import { _each, _includes, _trim } from './utils' +import { _each, _entries, _includes, _trim } from './utils' import { _isArray, _isNull, _isString, _isUndefined } from './utils/type-utils' import { logger } from './utils/logger' @@ -389,7 +389,7 @@ function elementsToString(elements: PHElement[]): string { el_string += `.${single_class.replace(/"/g, '')}` } } - let attributes: Record = { + const attributes: Record = { ...(element.text ? { text: element.text } : {}), 'nth-child': element.nth_child ?? 0, 'nth-of-type': element.nth_of_type ?? 0, @@ -397,13 +397,14 @@ function elementsToString(elements: PHElement[]): string { ...(element.attr_id ? { attr_id: element.attr_id } : {}), ...element.attributes, } - attributes = Object.fromEntries( - Object.entries(attributes) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([key, value]) => [escapeQuotes(key.toString()), escapeQuotes(value.toString())]) - ) + const sortedAttributes: Record = {} + _entries(attributes) + .sort(([a], [b]) => a.localeCompare(b)) + .forEach( + ([key, value]) => (sortedAttributes[escapeQuotes(key.toString())] = escapeQuotes(value.toString())) + ) el_string += ':' - el_string += Object.entries(attributes) + el_string += _entries(attributes) .map(([key, value]) => `${key}="${value}"`) .join('') return el_string @@ -412,16 +413,22 @@ function elementsToString(elements: PHElement[]): string { } function extractElements(elements: Properties[]): PHElement[] { - 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: extractAttrClass(el), - 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__'))), - })) + return elements.map((el) => { + const response = { + text: el['$el_text']?.slice(0, 400), + tag_name: el['tag_name'], + href: el['attr__href']?.slice(0, 2048), + attr_class: extractAttrClass(el), + attr_id: el['attr__id'], + nth_child: el['nth_child'], + nth_of_type: el['nth_of_type'], + attributes: {} as { [id: string]: any }, + } + _entries(el) + .filter(([key]) => key.startsWith('attr__')) + .forEach(([key, value]) => (response.attributes[key] = value)) + return response + }) } function extractAttrClass(el: Properties): PHElement['attr_class'] { From e22a9f4bd58da5318aaca7a45b15f08c31980126 Mon Sep 17 00:00:00 2001 From: Dave Murphy Date: Tue, 14 Nov 2023 11:01:11 -0800 Subject: [PATCH 07/11] Fix tests I broke --- src/__tests__/autocapture-utils.js | 2 +- src/__tests__/autocapture.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/__tests__/autocapture-utils.js b/src/__tests__/autocapture-utils.js index a540c7c63..8b09fa43b 100644 --- a/src/__tests__/autocapture-utils.js +++ b/src/__tests__/autocapture-utils.js @@ -410,7 +410,7 @@ describe(`Autocapture utility functions`, () => { { tag_name: 'div', nth_child: 1, nth_of_type: 2, $el_text: 'text' }, ]) - expect(elementChain).toEqual('div:nth-child="1"nth-of-type="2"text="text"') + expect(elementChain).toEqual('div:text="text"nth-child="1"nth-of-type="2"') }) }) }) diff --git a/src/__tests__/autocapture.js b/src/__tests__/autocapture.js index ea68e5852..28447e545 100644 --- a/src/__tests__/autocapture.js +++ b/src/__tests__/autocapture.js @@ -733,7 +733,7 @@ describe('Autocapture system', () => { const captureArgs = lib.capture.args[0] const props = captureArgs[1] expect(longString).toBe('prop'.repeat(400)) - expect(props['$elements_chain']).toContain('a:attr__data-props="' + 'prop'.repeat(256) + '..."') + expect(props['$elements_chain']).toContain('attr__data-props="' + 'prop'.repeat(256) + '..."') }) it('gets the href attribute from parent anchor tags', () => { @@ -750,7 +750,7 @@ describe('Autocapture system', () => { }, lib ) - expect(getCapturedProps(lib.capture)['$elements_chain']).toContain('a:attr__href="http://test.com"') + expect(getCapturedProps(lib.capture)['$elements_chain']).toContain('attr__href="http://test.com"') }) it('does not capture href attribute values from password elements', () => { From 863c82ffd1bab50ceff2704a408a8a23ec84848c Mon Sep 17 00:00:00 2001 From: Dave Murphy Date: Tue, 14 Nov 2023 11:29:00 -0800 Subject: [PATCH 08/11] `startsWith` -> `indexOf === 0` for IE11 --- src/autocapture-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocapture-utils.ts b/src/autocapture-utils.ts index 4043d8386..6029c6f3b 100644 --- a/src/autocapture-utils.ts +++ b/src/autocapture-utils.ts @@ -425,7 +425,7 @@ function extractElements(elements: Properties[]): PHElement[] { attributes: {} as { [id: string]: any }, } _entries(el) - .filter(([key]) => key.startsWith('attr__')) + .filter(([key]) => key.indexOf('attr__') === 0) .forEach(([key, value]) => (response.attributes[key] = value)) return response }) From c5e4a0e1a3ca72f7c82a23acab88ce176f907255 Mon Sep 17 00:00:00 2001 From: Dave Murphy Date: Thu, 16 Nov 2023 16:30:25 -0800 Subject: [PATCH 09/11] Hide change behind flag returned in decide response --- src/autocapture.ts | 10 +++++++--- src/posthog-core.ts | 6 ++++++ src/types.ts | 1 + 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/autocapture.ts b/src/autocapture.ts index d8f9f82ad..2d40981b2 100644 --- a/src/autocapture.ts +++ b/src/autocapture.ts @@ -255,9 +255,13 @@ const autocapture = { const props = _extend( this._getDefaultProperties(e.type), - { - $elements_chain: getElementsChainString(elementsJson), - }, + instance.elementsChainAsString + ? { + $elements_chain: getElementsChainString(elementsJson), + } + : { + $elements: elementsJson, + }, elementsJson[0]?.['$el_text'] ? { $el_text: elementsJson[0]?.['$el_text'] } : {}, this._getCustomProperties(targetElementList), autocaptureAugmentProperties diff --git a/src/posthog-core.ts b/src/posthog-core.ts index 7a844ed3d..d74a6713a 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -287,6 +287,7 @@ export class PostHog { __autocapture: boolean | AutocaptureConfig | undefined decideEndpointWasHit: boolean analyticsDefaultEndpoint: string + elementsChainAsString: boolean SentryIntegration: typeof SentryIntegration segmentIntegration: () => any @@ -310,6 +311,7 @@ export class PostHog { this.__autocapture = undefined this._jsc = function () {} as JSC this.analyticsDefaultEndpoint = '/e/' + this.elementsChainAsString = false this.featureFlags = new PostHogFeatureFlags(this) this.toolbar = new Toolbar(this) @@ -537,6 +539,10 @@ export class PostHog { if (response.analytics?.endpoint) { this.analyticsDefaultEndpoint = response.analytics.endpoint } + + if (response.analytics?.elementsChainAsString) { + this.elementsChainAsString = response.analytics.elementsChainAsString + } } _loaded(): void { diff --git a/src/types.ts b/src/types.ts index a413967a7..5ca54fea4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -238,6 +238,7 @@ export interface DecideResponse { capturePerformance?: boolean analytics?: { endpoint?: string + elementsChainAsString?: boolean } // this is currently in development and may have breaking changes without a major version bump autocaptureExceptions?: From 2e746c818da4320cf22d08b83daab73cbd441824 Mon Sep 17 00:00:00 2001 From: Dave Murphy Date: Fri, 17 Nov 2023 14:17:59 -0800 Subject: [PATCH 10/11] Align parsing of decide response --- src/posthog-core.ts | 4 ++-- src/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/posthog-core.ts b/src/posthog-core.ts index d74a6713a..c2d3bdff1 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -540,8 +540,8 @@ export class PostHog { this.analyticsDefaultEndpoint = response.analytics.endpoint } - if (response.analytics?.elementsChainAsString) { - this.elementsChainAsString = response.analytics.elementsChainAsString + if (response.elementsChainAsString) { + this.elementsChainAsString = response.elementsChainAsString } } diff --git a/src/types.ts b/src/types.ts index 5ca54fea4..85a0651cc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -238,8 +238,8 @@ export interface DecideResponse { capturePerformance?: boolean analytics?: { endpoint?: string - elementsChainAsString?: boolean } + elementsChainAsString?: boolean // this is currently in development and may have breaking changes without a major version bump autocaptureExceptions?: | boolean From 468801ee0c95d023af809d292e665bc7c73e8dfd Mon Sep 17 00:00:00 2001 From: Dave Murphy Date: Fri, 17 Nov 2023 15:13:45 -0800 Subject: [PATCH 11/11] Update tests --- src/__tests__/autocapture.js | 61 +++++++++++++++++++++++++---------- src/__tests__/posthog-core.js | 7 ++++ 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/__tests__/autocapture.js b/src/__tests__/autocapture.js index 28447e545..6535609f0 100644 --- a/src/__tests__/autocapture.js +++ b/src/__tests__/autocapture.js @@ -706,10 +706,10 @@ describe('Autocapture system', () => { const props = captureArgs[1] expect(event).toBe('$autocapture') expect(props['$event_type']).toBe('click') - expect(props['$elements_chain']).toContain('attr__href="http://test.com"') - expect(props['$elements_chain']).toContain('span:') - expect(props['$elements_chain']).toContain('div:') - expect(props['$elements_chain']).toContain('body:') + expect(props['$elements'][0]).toHaveProperty('attr__href', 'http://test.com') + expect(props['$elements'][1]).toHaveProperty('tag_name', 'span') + expect(props['$elements'][2]).toHaveProperty('tag_name', 'div') + expect(props['$elements'][props['$elements'].length - 1]).toHaveProperty('tag_name', 'body') }) it('truncate any element property value to 1024 bytes', () => { @@ -733,7 +733,7 @@ describe('Autocapture system', () => { const captureArgs = lib.capture.args[0] const props = captureArgs[1] expect(longString).toBe('prop'.repeat(400)) - expect(props['$elements_chain']).toContain('attr__data-props="' + 'prop'.repeat(256) + '..."') + expect(props['$elements'][0]).toHaveProperty('attr__data-props', 'prop'.repeat(256) + '...') }) it('gets the href attribute from parent anchor tags', () => { @@ -750,7 +750,7 @@ describe('Autocapture system', () => { }, lib ) - expect(getCapturedProps(lib.capture)['$elements_chain']).toContain('attr__href="http://test.com"') + expect(getCapturedProps(lib.capture)['$elements'][0]).toHaveProperty('attr__href', 'http://test.com') }) it('does not capture href attribute values from password elements', () => { @@ -784,7 +784,7 @@ describe('Autocapture system', () => { }, lib ) - expect(getCapturedProps(lib.capture)['$elements_chain']).not.toContain('a:attr__href') + expect(getCapturedProps(lib.capture)['$elements'][0]).not.toHaveProperty('attr__href') }) it('does not capture href attribute values that look like credit card numbers', () => { @@ -801,7 +801,7 @@ describe('Autocapture system', () => { }, lib ) - expect(getCapturedProps(lib.capture)['$elements_chain']).not.toContain('a:attr__href') + expect(getCapturedProps(lib.capture)['$elements'][0]).not.toHaveProperty('attr__href') }) it('does not capture href attribute values that look like social-security numbers', () => { @@ -818,7 +818,7 @@ describe('Autocapture system', () => { }, lib ) - expect(getCapturedProps(lib.capture)['$elements_chain']).not.toContain('a:attr__href') + expect(getCapturedProps(lib.capture)['$elements'][0]).not.toHaveProperty('attr__href') }) it('correctly identifies and formats text content', () => { @@ -866,7 +866,7 @@ describe('Autocapture system', () => { const props1 = getCapturedProps(lib.capture) const text1 = "Some super duper really long Text with new lines that we'll strip out and also we will want to make this text shorter since it's not likely people really care about text content that's super long and it also takes up more space and bandwidth. Some super d" - expect(props1['$elements_chain']).toContain(`text="${text1}"`) + expect(props1['$elements'][0]).toHaveProperty('$el_text', text1) expect(props1['$el_text']).toEqual(text1) lib.capture.resetHistory() @@ -876,7 +876,7 @@ describe('Autocapture system', () => { } autocapture._captureEvent(e2, lib) const props2 = getCapturedProps(lib.capture) - expect(props2['$elements_chain']).toContain('text="Some text"') + expect(props2['$elements'][0]).toHaveProperty('$el_text', 'Some text') expect(props2['$el_text']).toEqual('Some text') lib.capture.resetHistory() @@ -886,7 +886,8 @@ describe('Autocapture system', () => { } autocapture._captureEvent(e3, lib) const props3 = getCapturedProps(lib.capture) - expect(props3['$elements_chain']).not.toContain('text=""') + expect(props3['$elements'][0]).toHaveProperty('$el_text', '') + expect(props3).not.toHaveProperty('$el_text') }) it('does not capture sensitive text content', () => { @@ -915,7 +916,8 @@ describe('Autocapture system', () => { } autocapture._captureEvent(e1, lib) const props1 = getCapturedProps(lib.capture) - expect(props1['$elements_chain']).toContain('text="Why hello there"') + expect(props1['$elements'][0]).toHaveProperty('$el_text') + expect(props1['$elements'][0]['$el_text']).toMatch(/Why\s+hello\s+there/) lib.capture.resetHistory() const e2 = { @@ -924,7 +926,8 @@ describe('Autocapture system', () => { } autocapture._captureEvent(e2, lib) const props2 = getCapturedProps(lib.capture) - expect(props2['$elements_chain']).toContain('text="Why hello there"') + expect(props2['$elements'][0]).toHaveProperty('$el_text') + expect(props2['$elements'][0]['$el_text']).toMatch(/Why\s+hello\s+there/) lib.capture.resetHistory() const e3 = { @@ -933,7 +936,8 @@ describe('Autocapture system', () => { } autocapture._captureEvent(e3, lib) const props3 = getCapturedProps(lib.capture) - expect(props3['$elements_chain']).toContain('text="Why hello there"') + expect(props3['$elements'][0]).toHaveProperty('$el_text') + expect(props3['$elements'][0]['$el_text']).toMatch(/Why\s+hello\s+there/) }) it('should capture a submit event with form field props', () => { @@ -1027,7 +1031,7 @@ describe('Autocapture system', () => { autocapture._captureEvent(e1, newLib) const props1 = getCapturedProps(newLib.capture) - expect(props1['$elements_chain']).not.toContain('attr__formmethod') + expect('attr__formmethod' in props1['$elements'][0]).toEqual(false) }) it('does not capture any textContent if mask_all_text is set', () => { @@ -1056,7 +1060,30 @@ describe('Autocapture system', () => { autocapture._captureEvent(e1, newLib) const props1 = getCapturedProps(newLib.capture) - expect(props1['$elements_chain']).not.toHaveProperty('text') + expect(props1['$elements'][0]).not.toHaveProperty('$el_text') + }) + + it('returns elementsChain instead of elements when set', () => { + const elTarget = document.createElement('a') + elTarget.setAttribute('href', 'http://test.com') + const elParent = document.createElement('span') + elParent.appendChild(elTarget) + + const e = { + target: elTarget, + type: 'click', + } + + const newLib = { + ...lib, + elementsChainAsString: true, + } + + autocapture._captureEvent(e, newLib) + const props1 = getCapturedProps(newLib.capture) + + expect(props1['$elements_chain']).toBeDefined() + expect(props1['$elements']).toBeUndefined() }) }) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 73ef340ee..99f977506 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -276,6 +276,13 @@ describe('posthog core', () => { expect(given.lib.analyticsDefaultEndpoint).toEqual('/i/v0/e/') }) + + it('enables elementsChainAsString if given', () => { + given('decideResponse', () => ({ elementsChainAsString: true })) + given.subject() + + expect(given.lib.elementsChainAsString).toBe(true) + }) }) describe('_calculate_event_properties()', () => {