Skip to content

Commit

Permalink
Support events on buttons/links/forms in ShadowDOMs (#1351)
Browse files Browse the repository at this point in the history
* Support clicks on buttons/links in ShadowDOMs

* Detect form events within ShadowRoots
  • Loading branch information
jethron authored Oct 9, 2024
1 parent d888e38 commit 759f88d
Show file tree
Hide file tree
Showing 13 changed files with 277 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@snowplow/browser-plugin-button-click-tracking",
"comment": "Detect button clicks within ShadowRoots",
"type": "none"
}
],
"packageName": "@snowplow/browser-plugin-button-click-tracking"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@snowplow/browser-plugin-form-tracking",
"comment": "Detect form events within ShadowRoots",
"type": "none"
}
],
"packageName": "@snowplow/browser-plugin-form-tracking"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@snowplow/browser-plugin-link-click-tracking",
"comment": "Detect link clicks within ShadowRoots",
"type": "none"
}
],
"packageName": "@snowplow/browser-plugin-link-click-tracking"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@snowplow/javascript-tracker",
"comment": "",
"type": "none"
}
],
"packageName": "@snowplow/javascript-tracker"
}
2 changes: 1 addition & 1 deletion plugins/browser-plugin-button-click-tracking/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export function disableButtonClickTracking() {
* @param context - The dynamic context which will be evaluated for each button click event
*/
function eventHandler(event: MouseEvent, trackerId: string, filter: FilterFunction, context?: DynamicContext) {
let elem = event.target as HTMLElement | null;
let elem = (event.composed ? event.composedPath()[0] : event.target) as HTMLElement | null;
while (elem) {
if (elem instanceof HTMLButtonElement || (elem instanceof HTMLInputElement && elem.type === 'button')) {
if (filter(elem)) {
Expand Down
15 changes: 14 additions & 1 deletion plugins/browser-plugin-form-tracking/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,20 @@ function getFormChangeListener(
event_type: Exclude<FormTrackingEvent, FormTrackingEvent.SUBMIT_FORM>,
context?: DynamicContext | null
) {
return function ({ target }: Event) {
return function (e: Event) {
const target = e.composed ? e.composedPath()[0] : e.target;

// `change` and `submit` are not composed and are thus invisible to us
// bind late to the forms/field directly on field focus in this case
if (target !== e.target && e.composed && isTrackableElement(target)) {
if (target.form) {
if (_changeListeners[tracker.id]) addEventListener(target.form, 'change', _changeListeners[tracker.id], true);
if (_submitListeners[tracker.id]) addEventListener(target.form, 'submit', _submitListeners[tracker.id], true);
} else {
if (_changeListeners[tracker.id]) addEventListener(target, 'change', _changeListeners[tracker.id], true);
}
}

if (isTrackableElement(target) && config.fieldFilter(target)) {
let value: string | null = null;
let type: string | null = null;
Expand Down
71 changes: 71 additions & 0 deletions plugins/browser-plugin-form-tracking/test/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,77 @@ describe('FormTrackingPlugin', () => {
});
});

it('tracks from forms in shadowdom', async () => {
window.customElements.define(
'shadow-form',
class extends HTMLElement {
connectedCallback() {
const form = Object.assign(document.createElement('form'), { id: 'shadow-form' });

form.addEventListener(
'submit',
function (e) {
e.preventDefault();
},
false
);

const input = document.createElement('input');
form.appendChild(input);

const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(form);
}
}
);

const shadow = document.createElement('shadow-form');
document.body.appendChild(shadow);

enableFormTracking();

const target = shadow.shadowRoot!.querySelector('input')!;

target.focus();

target.value = 'changed';
target.dispatchEvent(new Event('change'));

target.form!.submit();

expect(
extractUeEvent('iglu:com.snowplowanalytics.snowplow/focus_form/jsonschema/1-0-0').from(
await eventStore.getAllPayloads()
)
).toMatchObject({
schema: 'iglu:com.snowplowanalytics.snowplow/focus_form/jsonschema/1-0-0',
data: {
formId: 'shadow-form',
},
});
expect(
extractUeEvent('iglu:com.snowplowanalytics.snowplow/change_form/jsonschema/1-0-0').from(
await eventStore.getAllPayloads()
)
).toMatchObject({
schema: 'iglu:com.snowplowanalytics.snowplow/change_form/jsonschema/1-0-0',
data: {
formId: 'shadow-form',
value: 'changed',
},
});
expect(
extractUeEvent('iglu:com.snowplowanalytics.snowplow/submit_form/jsonschema/1-0-0').from(
await eventStore.getAllPayloads()
)
).toMatchObject({
schema: 'iglu:com.snowplowanalytics.snowplow/submit_form/jsonschema/1-0-0',
data: {
formId: 'shadow-form',
},
});
});

it('associates non-nested forms correctly', async () => {
enableFormTracking();

Expand Down
4 changes: 3 additions & 1 deletion plugins/browser-plugin-link-click-tracking/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,9 @@ function clickHandler(tracker: string, evt: MouseEvent | undefined): void {
const event = evt || (window.event as MouseEvent);

const button = event.which || event.button;
const target = findNearestEligibleElement(event.target || event.srcElement);

const clicked = event.composed ? event.composedPath()[0] : event.target || event.srcElement;
const target = findNearestEligibleElement(clicked);

if (!target || target.href == null) return;
if (filter && !filter(target)) return;
Expand Down
34 changes: 34 additions & 0 deletions plugins/browser-plugin-link-click-tracking/test/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,40 @@ describe('LinkClickTrackingPlugin', () => {
});
});

it('tracks clicks on links in custom components', async () => {
enableLinkClickTracking();

window.customElements.define(
'shadow-link',
class extends HTMLElement {
connectedCallback() {
const a = document.createElement('a');
a.textContent = 'Shadow';
a.href = 'https://www.example.com/shadow';

const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(a);
}
}
);

const shadow = document.createElement('shadow-link');
document.body.appendChild(shadow);

shadow.shadowRoot!.querySelector('a')!.click();

expect(
extractUeEvent('iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1').from(
await eventStore.getAllPayloads()
)
).toMatchObject({
schema: 'iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1',
data: {
targetUrl: 'https://www.example.com/shadow',
},
});
});

it('doesnt double track clicks', async () => {
enableLinkClickTracking({ pseudoClicks: true });
enableLinkClickTracking({ pseudoClicks: false });
Expand Down
58 changes: 58 additions & 0 deletions trackers/javascript-tracker/test/integration/autoTracking.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import F from 'lodash/fp';
import { fetchResults } from '../micro';
import { pageSetup } from './helpers';
import { Key } from 'webdriverio';

const isMatchWithCallback = F.isMatchWith((lt, rt) => (F.isFunction(rt) ? rt(lt) : undefined));

Expand Down Expand Up @@ -304,6 +305,12 @@ describe('Auto tracking', () => {
await browser.switchToFrame(frame);
await $('#fname').click();

await loadUrlAndWait('/form-tracking.html?filter=shadow');
const input = await (await $('shadow-form')).shadow$('input');
await input.click();
await input.setValue('test');
await browser.keys(Key.Enter); // submit

// time for activity to register and request to arrive
await browser.pause(2500);

Expand Down Expand Up @@ -814,6 +821,57 @@ describe('Auto tracking', () => {
).toBe(true);
});

it('should track events from form in a shadowdom', () => {
expect(
logContains({
event: {
event: 'unstruct',
app_id: 'autotracking-form-' + testIdentifier,
page_url: 'http://snowplow-js-tracker.local:8080/form-tracking.html?filter=shadow',
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/focus_form/jsonschema/1-0-0',
},
},
},
})
).toBe(true);

expect(
logContains({
event: {
event: 'unstruct',
app_id: 'autotracking-form-' + testIdentifier,
page_url: 'http://snowplow-js-tracker.local:8080/form-tracking.html?filter=shadow',
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/change_form/jsonschema/1-0-0',
},
},
},
})
).toBe(true);

expect(
logContains({
event: {
event: 'unstruct',
app_id: 'autotracking-form-' + testIdentifier,
page_url: 'http://snowplow-js-tracker.local:8080/form-tracking.html?filter=shadow',
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/submit_form/jsonschema/1-0-0',
data: {
formId: 'shadow-form',
formClasses: ['shadow-form'],
},
},
},
},
})
).toBe(true);
});

it('should use transform function for pii field', () => {
expect(
logContains({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ describe('Snowplow Micro integration', () => {
await (await $('#button-child')).click();
await browser.pause(500);

// ShadowDOM
await (await $('#shadow')).shadow$('button').click();
await browser.pause(500);

// Disable/enable

await (await $('#disable')).click();
Expand Down Expand Up @@ -141,6 +145,11 @@ describe('Snowplow Micro integration', () => {
logContainsButtonClick(ev);
});

it('should get button when click was in a shadow dom', async () => {
const ev = makeEvent({ label: 'Shadow' }, method);
logContainsButtonClick(ev);
});

it('should not get disabled-click', () => {
const ev = makeEvent({ id: 'disabled-click', label: 'DisabledClick' }, method);
expect(logContains(ev)).toBe(false);
Expand Down
19 changes: 19 additions & 0 deletions trackers/javascript-tracker/test/pages/button-click-tracking.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@
<!-- Ensure button tracked when children are clicked -->
<button><span id="button-child">TestChildren</span></button>

<!-- Ensure button tracked when exists in ShadowDOM -->
<script>
window.customElements.define(
'shadow-btn',
class extends HTMLElement {
connectedCallback() {
const b = document.createElement('button');
b.type = 'button';
b.textContent = 'Shadow';

const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(b);
}
}
);
</script>

<shadow-btn id="shadow"></shadow-btn>

<!-- Enable/disable testing -->
<button id="disable" onclick="snowplow('disableButtonClickTracking')">Disable</button>
<button id="disabled-click">DisabledClick</button>
Expand Down
28 changes: 28 additions & 0 deletions trackers/javascript-tracker/test/pages/form-tracking.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,33 @@
iframeDocument.open();
iframeDocument.write(formHtml);
iframeDocument.close();

window.customElements.define(
'shadow-form',
class extends HTMLElement {
connectedCallback() {
const form = Object.assign(document.createElement('form'), { id: 'shadow-form', className: 'shadow-form' });

form.addEventListener(
'submit',
function (e) {
e.preventDefault();
},
false
);

const input = document.createElement('input');
form.appendChild(input);

const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(form);
}
}
);
</script>

<shadow-form></shadow-form>

<script>
(function (p, l, o, w, i, n, g) {
if (!p[i]) {
Expand Down Expand Up @@ -149,6 +174,9 @@
var forms = iframe.contentWindow.document.getElementsByTagName('form');
snowplow('enableFormTracking', { options: { forms: forms } });
break;
case 'shadow':
snowplow('enableFormTracking', { options: { forms: { allowlist: ['shadow-form'] } } });
break;
default:
snowplow('enableFormTracking', {
context: [
Expand Down

0 comments on commit 759f88d

Please sign in to comment.