Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support events on buttons/links/forms in ShadowDOMs #1351

Merged
merged 2 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading