From 28305b16a6361494ed4de5a4805dba520273e1c1 Mon Sep 17 00:00:00 2001 From: Alvin Wang Date: Fri, 14 Feb 2025 09:00:25 -0800 Subject: [PATCH] fix(analytics-browser): form tracking accesses element attributes through getAttribute (#959) --- .../src/plugins/form-interaction-tracking.ts | 20 +++++++++++++--- .../plugins/form-interaction-tracking.test.ts | 24 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/analytics-browser/src/plugins/form-interaction-tracking.ts b/packages/analytics-browser/src/plugins/form-interaction-tracking.ts index 8ff85db24..a51c3543f 100644 --- a/packages/analytics-browser/src/plugins/form-interaction-tracking.ts +++ b/packages/analytics-browser/src/plugins/form-interaction-tracking.ts @@ -62,29 +62,31 @@ export const formInteractionTracking = (): EnrichmentPlugin => { let hasFormChanged = false; addEventListener(form, 'change', () => { + const formDestination = extractFormAction(form); if (!hasFormChanged) { amplitude.track(DEFAULT_FORM_START_EVENT, { [FORM_ID]: stringOrUndefined(form.id), [FORM_NAME]: stringOrUndefined(form.name), - [FORM_DESTINATION]: form.action, + [FORM_DESTINATION]: formDestination, }); } hasFormChanged = true; }); addEventListener(form, 'submit', () => { + const formDestination = extractFormAction(form); if (!hasFormChanged) { amplitude.track(DEFAULT_FORM_START_EVENT, { [FORM_ID]: stringOrUndefined(form.id), [FORM_NAME]: stringOrUndefined(form.name), - [FORM_DESTINATION]: form.action, + [FORM_DESTINATION]: formDestination, }); } amplitude.track(DEFAULT_FORM_SUBMIT_EVENT, { [FORM_ID]: stringOrUndefined(form.id), [FORM_NAME]: stringOrUndefined(form.name), - [FORM_DESTINATION]: form.action, + [FORM_DESTINATION]: formDestination, }); hasFormChanged = false; }); @@ -143,3 +145,15 @@ export const stringOrUndefined = (name: T): T extends string ? string : undef return name as T extends string ? string : undefined; }; + +// Extracts the form action attribute, and normalizes it to a valid URL to preserve the previous behavior of accessing the action property directly. +export const extractFormAction = (form: HTMLFormElement): string | null => { + let formDestination = form.getAttribute('action'); + try { + // eslint-disable-next-line no-restricted-globals + formDestination = new URL(encodeURI(formDestination ?? ''), window.location.href).href; + } catch { + // + } + return formDestination; +}; diff --git a/packages/analytics-browser/test/plugins/form-interaction-tracking.test.ts b/packages/analytics-browser/test/plugins/form-interaction-tracking.test.ts index 43f7802fe..dd2cf2e24 100644 --- a/packages/analytics-browser/test/plugins/form-interaction-tracking.test.ts +++ b/packages/analytics-browser/test/plugins/form-interaction-tracking.test.ts @@ -70,6 +70,30 @@ describe('formInteractionTracking', () => { expect(amplitude.track).toHaveBeenCalledTimes(1); }); + test('should return current location if form action attribute is missing', async () => { + // setup + const config = createConfigurationMock(); + const plugin = formInteractionTracking(); + await plugin.setup?.(config, amplitude); + window.dispatchEvent(new Event('load')); + + // Remove form action + document.getElementById('my-form-id')?.removeAttribute('action'); + // trigger change event + document.getElementById('my-form-id')?.dispatchEvent(new Event('change')); + // assert first event was tracked + expect(amplitude.track).toHaveBeenCalledTimes(1); + expect(amplitude.track).toHaveBeenNthCalledWith(1, '[Amplitude] Form Started', { + [FORM_ID]: 'my-form-id', + [FORM_NAME]: 'my-form-name', + [FORM_DESTINATION]: 'http://localhost/', + }); + // trigger change event again + document.getElementById('my-form-id')?.dispatchEvent(new Event('change')); + // assert second event was not tracked + expect(amplitude.track).toHaveBeenCalledTimes(1); + }); + test('should track form_start event for a dynamically added form tag', async () => { // setup const config = createConfigurationMock();