-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add form interaction event tracker plugin (#323)
- Loading branch information
1 parent
1604409
commit f6ee918
Showing
4 changed files
with
250 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
87 changes: 87 additions & 0 deletions
87
packages/analytics-browser/src/plugins/form-interaction-tracking.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { BrowserClient, PluginType, Event, EnrichmentPlugin } from '@amplitude/analytics-types'; | ||
import { BrowserConfig } from '../config'; | ||
|
||
const FORM_START_EVENT = 'form_start'; | ||
const FORM_SUBMIT_EVENT = 'form_submit'; | ||
|
||
export const formInteractionTracking = (): EnrichmentPlugin => { | ||
const name = 'formInteractionTracking'; | ||
const type = PluginType.ENRICHMENT; | ||
const setup = async (config: BrowserConfig, amplitude?: BrowserClient) => { | ||
/* istanbul ignore if */ | ||
if (!amplitude) { | ||
// TODO: Add required minimum version of @amplitude/analytics-browser | ||
config.loggerProvider.warn( | ||
'Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked.', | ||
); | ||
return; | ||
} | ||
|
||
const addFormInteractionListener = (form: HTMLFormElement) => { | ||
let hasFormChanged = false; | ||
|
||
form.addEventListener( | ||
'change', | ||
() => { | ||
if (!hasFormChanged) { | ||
amplitude.track(FORM_START_EVENT, { | ||
form_id: form.id, | ||
form_name: form.name, | ||
form_destination: form.action, | ||
}); | ||
} | ||
hasFormChanged = true; | ||
}, | ||
{}, | ||
); | ||
|
||
form.addEventListener('submit', () => { | ||
if (!hasFormChanged) { | ||
amplitude.track(FORM_START_EVENT, { | ||
form_id: form.id, | ||
form_name: form.name, | ||
form_destination: form.action, | ||
}); | ||
} | ||
|
||
amplitude.track(FORM_SUBMIT_EVENT, { | ||
form_id: form.id, | ||
form_name: form.name, | ||
form_destination: form.action, | ||
}); | ||
hasFormChanged = false; | ||
}); | ||
}; | ||
|
||
// Adds listener to existing anchor tags | ||
const forms = Array.from(document.getElementsByTagName('form')); | ||
forms.forEach(addFormInteractionListener); | ||
|
||
// Adds listener to anchor tags added after initial load | ||
/* istanbul ignore else */ | ||
if (typeof MutationObserver !== 'undefined') { | ||
const observer = new MutationObserver((mutations) => { | ||
mutations.forEach((mutation) => { | ||
mutation.addedNodes.forEach((node) => { | ||
if (node.nodeName === 'FORM') { | ||
addFormInteractionListener(node as HTMLFormElement); | ||
} | ||
}); | ||
}); | ||
}); | ||
|
||
observer.observe(document.body, { | ||
subtree: true, | ||
childList: true, | ||
}); | ||
} | ||
}; | ||
const execute = async (event: Event) => event; | ||
|
||
return { | ||
name, | ||
type, | ||
setup, | ||
execute, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
162 changes: 162 additions & 0 deletions
162
packages/analytics-browser/test/plugins/form-interaction-tracking.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
/* eslint-disable @typescript-eslint/unbound-method */ | ||
|
||
import { createAmplitudeMock, createConfigurationMock } from '../helpers/mock'; | ||
import { formInteractionTracking } from '../../src/plugins/form-interaction-tracking'; | ||
|
||
describe('formInteractionTracking', () => { | ||
let amplitude = createAmplitudeMock(); | ||
|
||
beforeEach(() => { | ||
amplitude = createAmplitudeMock(); | ||
|
||
const form = document.createElement('form'); | ||
form.setAttribute('id', 'my-form-id'); | ||
form.setAttribute('name', 'my-form-name'); | ||
form.setAttribute('action', '/submit'); | ||
|
||
const text = document.createElement('input'); | ||
text.setAttribute('type', 'text'); | ||
text.setAttribute('id', 'my-text-id'); | ||
|
||
const submit = document.createElement('input'); | ||
submit.setAttribute('type', 'submit'); | ||
submit.setAttribute('id', 'my-submit-id'); | ||
|
||
form.appendChild(text); | ||
form.appendChild(submit); | ||
document.body.appendChild(form); | ||
}); | ||
|
||
afterEach(() => { | ||
document.querySelector('form#my-form-id')?.remove(); | ||
}); | ||
|
||
test('should track form_start event', async () => { | ||
// setup | ||
const config = createConfigurationMock(); | ||
const plugin = formInteractionTracking(); | ||
await plugin.setup(config, amplitude); | ||
|
||
// 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, 'form_start', { | ||
form_id: 'my-form-id', | ||
form_name: 'my-form-name', | ||
form_destination: 'http://localhost/submit', | ||
}); | ||
|
||
// 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(); | ||
const plugin = formInteractionTracking(); | ||
await plugin.setup(config, amplitude); | ||
|
||
// add form elemen dynamically | ||
const form = document.createElement('form'); | ||
form.setAttribute('id', 'my-form-2-id'); | ||
form.setAttribute('name', 'my-form-2-name'); | ||
form.setAttribute('action', '/submit'); | ||
|
||
const text = document.createElement('input'); | ||
text.setAttribute('type', 'text'); | ||
text.setAttribute('id', 'my-text-2-id'); | ||
|
||
const submit = document.createElement('input'); | ||
submit.setAttribute('type', 'submit'); | ||
submit.setAttribute('id', 'my-submit-2-id'); | ||
|
||
form.appendChild(text); | ||
form.appendChild(submit); | ||
document.body.appendChild(form); | ||
|
||
// allow mutation observer to execute and event listener to be attached | ||
await new Promise((r) => r(undefined)); // basically, await next clock tick | ||
// trigger change event | ||
form.dispatchEvent(new Event('change')); | ||
|
||
// assert first event was tracked | ||
expect(amplitude.track).toHaveBeenCalledTimes(1); | ||
expect(amplitude.track).toHaveBeenNthCalledWith(1, 'form_start', { | ||
form_id: 'my-form-2-id', | ||
form_name: 'my-form-2-name', | ||
form_destination: 'http://localhost/submit', | ||
}); | ||
|
||
// trigger change event again | ||
form.dispatchEvent(new Event('change')); | ||
|
||
// assert second event was not tracked | ||
expect(amplitude.track).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
test('should track form_start and form_submit events on change and submit', async () => { | ||
// setup | ||
const config = createConfigurationMock(); | ||
const plugin = formInteractionTracking(); | ||
await plugin.setup(config, amplitude); | ||
|
||
// trigger change event | ||
document.getElementById('my-form-id')?.dispatchEvent(new Event('submit')); | ||
|
||
// assert both events were tracked | ||
expect(amplitude.track).toHaveBeenCalledTimes(2); | ||
expect(amplitude.track).toHaveBeenNthCalledWith(1, 'form_start', { | ||
form_id: 'my-form-id', | ||
form_name: 'my-form-name', | ||
form_destination: 'http://localhost/submit', | ||
}); | ||
expect(amplitude.track).toHaveBeenNthCalledWith(2, 'form_submit', { | ||
form_id: 'my-form-id', | ||
form_name: 'my-form-name', | ||
form_destination: 'http://localhost/submit', | ||
}); | ||
}); | ||
|
||
test('should track form_start and form_submit events on submit only', async () => { | ||
// setup | ||
const config = createConfigurationMock(); | ||
const plugin = formInteractionTracking(); | ||
await plugin.setup(config, amplitude); | ||
|
||
// trigger change event again | ||
document.getElementById('my-form-id')?.dispatchEvent(new Event('change')); | ||
|
||
// assert first event was tracked | ||
expect(amplitude.track).toHaveBeenCalledTimes(1); | ||
expect(amplitude.track).toHaveBeenNthCalledWith(1, 'form_start', { | ||
form_id: 'my-form-id', | ||
form_name: 'my-form-name', | ||
form_destination: 'http://localhost/submit', | ||
}); | ||
|
||
// trigger submit event | ||
document.getElementById('my-form-id')?.dispatchEvent(new Event('submit')); | ||
|
||
// assert second event was tracked | ||
expect(amplitude.track).toHaveBeenCalledTimes(2); | ||
expect(amplitude.track).toHaveBeenNthCalledWith(2, 'form_submit', { | ||
form_id: 'my-form-id', | ||
form_name: 'my-form-name', | ||
form_destination: 'http://localhost/submit', | ||
}); | ||
}); | ||
|
||
test('should not enrich events', async () => { | ||
const input = { | ||
event_type: 'page_view', | ||
}; | ||
const plugin = formInteractionTracking(); | ||
const result = await plugin.execute(input); | ||
expect(result).toEqual(input); | ||
}); | ||
}); |