From c136619d1a01980b0a793c62b8b2c8bcfbd9e0bc Mon Sep 17 00:00:00 2001 From: adatzer Date: Fri, 29 Sep 2023 17:49:43 +0300 Subject: [PATCH] Fix transform function for submit_form event (close #1241) --- .../src/helpers.ts | 67 +++++++----------- .../browser-plugin-form-tracking/src/index.ts | 30 -------- .../test/integration/autoTracking.test.ts | 70 +++++++++++++++++++ .../test/pages/form-tracking.html | 17 +++++ 4 files changed, 112 insertions(+), 72 deletions(-) diff --git a/plugins/browser-plugin-form-tracking/src/helpers.ts b/plugins/browser-plugin-form-tracking/src/helpers.ts index da21e3f4c..4b424dd04 100644 --- a/plugins/browser-plugin-form-tracking/src/helpers.ts +++ b/plugins/browser-plugin-form-tracking/src/helpers.ts @@ -1,33 +1,3 @@ -/* - * Copyright (c) 2022 Snowplow Analytics Ltd, 2010 Anthon Pang - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - import { getCssClasses, addEventListener, @@ -90,12 +60,18 @@ export interface ElementData extends Record { type?: string; } -export type transformFn = (x: string | null, elt: ElementData | TrackedHTMLElement) => string | null; +export type transformFn = ( + elementValue: string | null, + elementInfo: ElementData | TrackedHTMLElement, + elt: TrackedHTMLElement +) => string | null; export const innerElementTags: Array = ['textarea', 'input', 'select']; type TrackedHTMLElementWithMarker = TrackedHTMLElement & Record; +type ElementDataWrapper = { elementData: ElementData; originalElement: TrackedHTMLElement }; + const defaultTransformFn: transformFn = (x) => x; interface FormConfiguration { @@ -242,7 +218,7 @@ function getParentFormIdentifier(elt: Node | null) { * Returns a list of the input, textarea, and select elements inside a form along with their values */ function getInnerFormElements(trackingMarker: string, elt: HTMLFormElement) { - var innerElements: Array = []; + var innerElements: Array = []; Array.prototype.slice.call(innerElementTags).forEach((tagname: 'textarea' | 'input' | 'select') => { let trackedChildren = Array.prototype.slice.call(elt.getElementsByTagName(tagname)).filter(function (child) { return child.hasOwnProperty(trackingMarker); @@ -252,17 +228,20 @@ function getInnerFormElements(trackingMarker: string, elt: HTMLFormElement) { if (child.type === 'submit') { return; } - var elementJson: ElementData = { - name: getElementIdentifier(child), - value: child.value, - nodeName: child.nodeName, + var elementJson: ElementDataWrapper = { + elementData: { + name: getElementIdentifier(child), + value: child.value, + nodeName: child.nodeName, + }, + originalElement: child, }; if (child.type && child.nodeName.toUpperCase() === 'INPUT') { - elementJson.type = child.type; + elementJson.elementData.type = child.type; } if ((child.type === 'checkbox' || child.type === 'radio') && !(child as HTMLInputElement).checked) { - elementJson.value = null; + elementJson.elementData.value = null; } innerElements.push(elementJson); }); @@ -285,7 +264,9 @@ function getFormChangeListener( if (elt) { var type = elt.nodeName && elt.nodeName.toUpperCase() === 'INPUT' ? elt.type : null; var value = - elt.type === 'checkbox' && !(elt as HTMLInputElement).checked ? null : config.fieldTransform(elt.value, elt); + elt.type === 'checkbox' && !(elt as HTMLInputElement).checked + ? null + : config.fieldTransform(elt.value, elt, elt); if (event_type === 'change_form' || (type !== 'checkbox' && type !== 'radio')) { tracker.core.track( buildFormFocusOrChange({ @@ -317,15 +298,17 @@ function getFormSubmissionListener( var elt = e.target as HTMLFormElement; var innerElements = getInnerFormElements(trackingMarker, elt); innerElements.forEach(function (innerElement) { - innerElement.value = config.fieldTransform(innerElement.value, innerElement) ?? innerElement.value; + var eltData = innerElement.elementData; + eltData.value = config.fieldTransform(eltData.value, eltData, innerElement.originalElement) ?? eltData.value; }); + var elementsData = innerElements.map((elt) => elt.elementData); tracker.core.track( buildFormSubmission({ formId: getElementIdentifier(elt) ?? '', formClasses: getCssClasses(elt), - elements: innerElements, + elements: elementsData, }), - resolveDynamicContext(context, elt, innerElements) + resolveDynamicContext(context, elt, elementsData) ); }; } diff --git a/plugins/browser-plugin-form-tracking/src/index.ts b/plugins/browser-plugin-form-tracking/src/index.ts index 24ddfcc4a..3f3f79a39 100644 --- a/plugins/browser-plugin-form-tracking/src/index.ts +++ b/plugins/browser-plugin-form-tracking/src/index.ts @@ -1,33 +1,3 @@ -/* - * Copyright (c) 2022 Snowplow Analytics Ltd, 2010 Anthon Pang - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - import { BrowserPlugin, BrowserTracker } from '@snowplow/browser-tracker-core'; import { addFormListeners, FormTrackingConfiguration } from './helpers'; diff --git a/trackers/javascript-tracker/test/integration/autoTracking.test.ts b/trackers/javascript-tracker/test/integration/autoTracking.test.ts index 7b3b885db..ca9c28005 100755 --- a/trackers/javascript-tracker/test/integration/autoTracking.test.ts +++ b/trackers/javascript-tracker/test/integration/autoTracking.test.ts @@ -280,6 +280,11 @@ describe('Auto tracking', () => { await $('#fname').click(); await $('#lname').click(); + await loadUrlAndWait('/form-tracking.html?filter=transform'); + + await $('#pid').click(); + await $('#submit').click(); + await browser.pause(1000); await loadUrlAndWait('/form-tracking.html?filter=excludedForm'); @@ -808,4 +813,69 @@ describe('Auto tracking', () => { }) ).toBe(true); }); + + it('should use transform function for pii field', () => { + expect( + logContains({ + event: { + event: 'unstruct', + app_id: 'autotracking-form-' + testIdentifier, + unstruct_event: { + data: { + schema: 'iglu:com.snowplowanalytics.snowplow/focus_form/jsonschema/1-0-0', + data: { + formId: 'myForm', + elementId: 'pname', + nodeName: 'INPUT', + elementClasses: [], + value: 'redacted', + elementType: 'text', + }, + }, + }, + }, + }) + ).toBe(true); + + expect( + logContains({ + event: { + event: 'unstruct', + app_id: 'autotracking-form-' + testIdentifier, + unstruct_event: { + data: { + schema: 'iglu:com.snowplowanalytics.snowplow/submit_form/jsonschema/1-0-0', + data: { + formId: 'myForm', + formClasses: ['formy-mcformface'], + elements: [ + { + name: 'message', + value: 'This is a message', + nodeName: 'TEXTAREA', + }, + { + name: 'fname', + value: 'John', + nodeName: 'INPUT', + type: 'text', + }, + { name: 'lname', value: 'Doe', nodeName: 'INPUT', type: 'text' }, + { + name: 'pname', + value: 'redacted', + nodeName: 'INPUT', + type: 'text', + }, + { name: 'vehicle', value: null, nodeName: 'INPUT', type: 'radio' }, + { name: 'terms', value: null, nodeName: 'INPUT', type: 'checkbox' }, + { name: 'cars', value: 'volvo', nodeName: 'SELECT' }, + ], + }, + }, + }, + }, + }) + ).toBe(true); + }); }); diff --git a/trackers/javascript-tracker/test/pages/form-tracking.html b/trackers/javascript-tracker/test/pages/form-tracking.html index 627f067d1..3717b0d17 100644 --- a/trackers/javascript-tracker/test/pages/form-tracking.html +++ b/trackers/javascript-tracker/test/pages/form-tracking.html @@ -41,6 +41,10 @@



+ +
+

+
@@ -106,6 +110,12 @@ function formFilter(formElement) { return formElement.id !== 'lname'; } + function redactPII(eltValue, _, elt) { + if (elt.id === 'pid') { + return 'redacted'; + } + return eltValue; + } switch (parseQuery().filter) { case 'exclude': @@ -122,6 +132,13 @@ }, }); break; + case 'transform': + snowplow('enableFormTracking', { + options: { + fields: { transform: redactPII }, + }, + }); + break; case 'excludedForm': snowplow('enableFormTracking', { options: { forms: { denylist: ['excluded-form'] } } }); break;