@technoapple/ga4 1.0.4 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/node.js.yml +31 -31
- package/.prettierignore +1 -1
- package/LICENSE +21 -21
- package/README.md +386 -48
- package/REQUIREMENTS.md +548 -0
- package/babel.config.js +5 -5
- package/build/main/ga4/ga4.d.ts +13 -0
- package/build/main/ga4/ga4.js +24 -1
- package/build/main/helpers/debounce.d.ts +5 -0
- package/build/main/helpers/debounce.js +23 -0
- package/build/main/helpers/delegate.d.ts +8 -0
- package/build/main/helpers/delegate.js +37 -0
- package/build/main/helpers/dom-ready.d.ts +1 -0
- package/build/main/helpers/dom-ready.js +13 -0
- package/build/main/helpers/parse-url.d.ts +11 -0
- package/build/main/helpers/parse-url.js +32 -0
- package/build/main/helpers/session.d.ts +4 -0
- package/build/main/helpers/session.js +50 -0
- package/build/main/index.d.ts +9 -0
- package/build/main/index.js +19 -2
- package/build/main/plugins/clean-url-tracker.d.ts +17 -0
- package/build/main/plugins/clean-url-tracker.js +105 -0
- package/build/main/plugins/event-tracker.d.ts +27 -0
- package/build/main/plugins/event-tracker.js +76 -0
- package/build/main/plugins/impression-tracker.d.ts +32 -0
- package/build/main/plugins/impression-tracker.js +202 -0
- package/build/main/plugins/index.d.ts +8 -0
- package/build/main/plugins/index.js +20 -0
- package/build/main/plugins/media-query-tracker.d.ts +20 -0
- package/build/main/plugins/media-query-tracker.js +96 -0
- package/build/main/plugins/outbound-form-tracker.d.ts +17 -0
- package/build/main/plugins/outbound-form-tracker.js +55 -0
- package/build/main/plugins/outbound-link-tracker.d.ts +19 -0
- package/build/main/plugins/outbound-link-tracker.js +63 -0
- package/build/main/plugins/page-visibility-tracker.d.ts +24 -0
- package/build/main/plugins/page-visibility-tracker.js +93 -0
- package/build/main/plugins/url-change-tracker.d.ts +20 -0
- package/build/main/plugins/url-change-tracker.js +76 -0
- package/build/main/types/plugins.d.ts +78 -0
- package/build/main/types/plugins.js +3 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/docs/examples/react.md +95 -0
- package/docs/examples/vanilla.md +65 -0
- package/docs/examples/vue.md +87 -0
- package/jest.config.ts +195 -195
- package/package.json +56 -56
- package/src/dataLayer.ts +85 -85
- package/src/ga4/ga4.ts +69 -40
- package/src/ga4/ga4option.ts +4 -4
- package/src/ga4/index.ts +4 -4
- package/src/helpers/debounce.ts +28 -0
- package/src/helpers/delegate.ts +51 -0
- package/src/helpers/dom-ready.ts +7 -0
- package/src/helpers/parse-url.ts +37 -0
- package/src/helpers/session.ts +39 -0
- package/src/index.ts +34 -7
- package/src/plugins/clean-url-tracker.ts +112 -0
- package/src/plugins/event-tracker.ts +90 -0
- package/src/plugins/impression-tracker.ts +230 -0
- package/src/plugins/index.ts +8 -0
- package/src/plugins/media-query-tracker.ts +116 -0
- package/src/plugins/outbound-form-tracker.ts +65 -0
- package/src/plugins/outbound-link-tracker.ts +72 -0
- package/src/plugins/page-visibility-tracker.ts +104 -0
- package/src/plugins/url-change-tracker.ts +84 -0
- package/src/types/dataLayer.ts +9 -9
- package/src/types/global.ts +12 -12
- package/src/types/gtag.ts +259 -259
- package/src/types/plugins.ts +98 -0
- package/src/util.ts +18 -18
- package/test/dataLayer.spec.ts +55 -55
- package/test/ga4.spec.ts +36 -36
- package/tsconfig.json +28 -28
- package/tsconfig.module.json +11 -11
package/src/dataLayer.ts
CHANGED
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
import { DataLayerObject } from './types/dataLayer';
|
|
2
|
-
|
|
3
|
-
function isObject(target:any): boolean {
|
|
4
|
-
if (!target) {
|
|
5
|
-
return false;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
return Object.prototype.toString.call(target) === '[object Object]';
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function isArguments(target:object): boolean {
|
|
12
|
-
if (!target) {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return Object.prototype.toString.call(target) === '[object Arguments]';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function getDataValue(key:string, currentData: DataLayerObject) {
|
|
20
|
-
if (isObject(currentData)) {
|
|
21
|
-
const data = currentData[key];
|
|
22
|
-
if (data) {
|
|
23
|
-
return data;
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
else if (isArguments(currentData) || Array.isArray(currentData)) {
|
|
28
|
-
const arr = Object.values(currentData);
|
|
29
|
-
const data = arr.find(c => c === key);
|
|
30
|
-
if (data) {
|
|
31
|
-
return data;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const dataObj = arr.find(c => isObject(c));
|
|
35
|
-
if (dataObj) {
|
|
36
|
-
const data = dataObj[key];
|
|
37
|
-
if (data) {
|
|
38
|
-
return data;
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
// not support.
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* get value from dataLayer
|
|
53
|
-
* @param key key to search from dataLayer
|
|
54
|
-
* @param getLast boolean, false (default) find the first item, true search the last value for the same key
|
|
55
|
-
* @returns return the value if find, otherwise return empty string;
|
|
56
|
-
*/
|
|
57
|
-
function get(key:string, getLast?:boolean): any {
|
|
58
|
-
|
|
59
|
-
if (!window.dataLayer || !Array.isArray(window.dataLayer)) {
|
|
60
|
-
return '';
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (!getLast) {
|
|
64
|
-
|
|
65
|
-
for (let index = 0; index < window.dataLayer.length; index++) {
|
|
66
|
-
const data = getDataValue(key, window.dataLayer[index]);
|
|
67
|
-
if (!data) {
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return data;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
for (let index = window.dataLayer.length; index > 0; index--) {
|
|
76
|
-
const data = getDataValue(key, window.dataLayer[index]);
|
|
77
|
-
if (!data) {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return data;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
1
|
+
import { DataLayerObject } from './types/dataLayer';
|
|
2
|
+
|
|
3
|
+
function isObject(target:any): boolean {
|
|
4
|
+
if (!target) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return Object.prototype.toString.call(target) === '[object Object]';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isArguments(target:object): boolean {
|
|
12
|
+
if (!target) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return Object.prototype.toString.call(target) === '[object Arguments]';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getDataValue(key:string, currentData: DataLayerObject) {
|
|
20
|
+
if (isObject(currentData)) {
|
|
21
|
+
const data = currentData[key];
|
|
22
|
+
if (data) {
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
else if (isArguments(currentData) || Array.isArray(currentData)) {
|
|
28
|
+
const arr = Object.values(currentData);
|
|
29
|
+
const data = arr.find(c => c === key);
|
|
30
|
+
if (data) {
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const dataObj = arr.find(c => isObject(c));
|
|
35
|
+
if (dataObj) {
|
|
36
|
+
const data = dataObj[key];
|
|
37
|
+
if (data) {
|
|
38
|
+
return data;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// not support.
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* get value from dataLayer
|
|
53
|
+
* @param key key to search from dataLayer
|
|
54
|
+
* @param getLast boolean, false (default) find the first item, true search the last value for the same key
|
|
55
|
+
* @returns return the value if find, otherwise return empty string;
|
|
56
|
+
*/
|
|
57
|
+
function get(key:string, getLast?:boolean): any {
|
|
58
|
+
|
|
59
|
+
if (!window.dataLayer || !Array.isArray(window.dataLayer)) {
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!getLast) {
|
|
64
|
+
|
|
65
|
+
for (let index = 0; index < window.dataLayer.length; index++) {
|
|
66
|
+
const data = getDataValue(key, window.dataLayer[index]);
|
|
67
|
+
if (!data) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
for (let index = window.dataLayer.length; index > 0; index--) {
|
|
76
|
+
const data = getDataValue(key, window.dataLayer[index]);
|
|
77
|
+
if (!data) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return data;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
86
|
export {get};
|
package/src/ga4/ga4.ts
CHANGED
|
@@ -1,41 +1,70 @@
|
|
|
1
|
-
import { ga4Option } from "./ga4option";
|
|
2
|
-
import { DataLayerObject } from "../types/dataLayer";
|
|
3
|
-
import { KeyValueParams, gtag } from "../types/gtag";
|
|
4
|
-
import {} from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
private
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
1
|
+
import { ga4Option } from "./ga4option";
|
|
2
|
+
import { DataLayerObject } from "../types/dataLayer";
|
|
3
|
+
import { KeyValueParams, gtag } from "../types/gtag";
|
|
4
|
+
import { GA4Plugin, SendFunction } from "../types/plugins";
|
|
5
|
+
import {} from '../types/global';
|
|
6
|
+
|
|
7
|
+
class ga4 {
|
|
8
|
+
|
|
9
|
+
private static instance: ga4;
|
|
10
|
+
private _plugins: GA4Plugin[] = [];
|
|
11
|
+
|
|
12
|
+
private constructor() {
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public init(option:ga4Option){
|
|
16
|
+
window.dataLayer = window.dataLayer || Array<DataLayerObject>;
|
|
17
|
+
window.gtag = window.gtag || function() {
|
|
18
|
+
window.dataLayer.push(arguments);
|
|
19
|
+
}
|
|
20
|
+
window.gtag('js', new Date());
|
|
21
|
+
window.gtag('config', option.targetId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public static getInstance():ga4 {
|
|
25
|
+
if (!ga4.instance) {
|
|
26
|
+
ga4.instance = new ga4();
|
|
27
|
+
}
|
|
28
|
+
return ga4.instance;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public send(eventName:string, eventParameters: KeyValueParams ): boolean {
|
|
32
|
+
|
|
33
|
+
window.gtag('event', eventName, eventParameters);
|
|
34
|
+
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register a plugin with the GA4 instance.
|
|
40
|
+
* @param PluginClass The plugin class constructor
|
|
41
|
+
* @param options Plugin-specific configuration options
|
|
42
|
+
* @returns The plugin instance (call `.remove()` to unregister)
|
|
43
|
+
*/
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
public use<T extends GA4Plugin>(
|
|
46
|
+
PluginClass: new (send: SendFunction, options?: any) => T,
|
|
47
|
+
options?: any
|
|
48
|
+
): T {
|
|
49
|
+
const send: SendFunction = (eventName: string, params: Record<string, unknown>) => {
|
|
50
|
+
window.gtag('event', eventName, params as KeyValueParams);
|
|
51
|
+
};
|
|
52
|
+
const plugin = new PluginClass(send, options);
|
|
53
|
+
this._plugins.push(plugin);
|
|
54
|
+
return plugin;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Remove all registered plugins and clean up their listeners.
|
|
59
|
+
*/
|
|
60
|
+
public removeAll(): void {
|
|
61
|
+
this._plugins.forEach(p => p.remove());
|
|
62
|
+
this._plugins = [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get gtag() : gtag {
|
|
66
|
+
return window.gtag;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
41
70
|
export {ga4};
|
package/src/ga4/ga4option.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
interface ga4Option {
|
|
2
|
-
targetId:string;
|
|
3
|
-
}
|
|
4
|
-
|
|
1
|
+
interface ga4Option {
|
|
2
|
+
targetId:string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
5
|
export {ga4Option};
|
package/src/ga4/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {ga4} from './ga4';
|
|
2
|
-
|
|
3
|
-
const ga = ga4.getInstance();
|
|
4
|
-
|
|
1
|
+
import {ga4} from './ga4';
|
|
2
|
+
|
|
3
|
+
const ga = ga4.getInstance();
|
|
4
|
+
|
|
5
5
|
export {ga};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface DebouncedFunction<T extends (...args: never[]) => void> {
|
|
2
|
+
(...args: Parameters<T>): void;
|
|
3
|
+
cancel(): void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function debounce<T extends (...args: never[]) => void>(
|
|
7
|
+
fn: T,
|
|
8
|
+
delay: number
|
|
9
|
+
): DebouncedFunction<T> {
|
|
10
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
11
|
+
|
|
12
|
+
const debounced = function (this: unknown, ...args: Parameters<T>) {
|
|
13
|
+
if (timer !== null) clearTimeout(timer);
|
|
14
|
+
timer = setTimeout(() => {
|
|
15
|
+
timer = null;
|
|
16
|
+
fn.apply(this, args);
|
|
17
|
+
}, delay);
|
|
18
|
+
} as DebouncedFunction<T>;
|
|
19
|
+
|
|
20
|
+
debounced.cancel = () => {
|
|
21
|
+
if (timer !== null) {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
timer = null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return debounced;
|
|
28
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface DelegateHandle {
|
|
2
|
+
destroy(): void;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface DelegateOptions {
|
|
6
|
+
composed?: boolean;
|
|
7
|
+
useCapture?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function delegate(
|
|
11
|
+
target: EventTarget,
|
|
12
|
+
eventType: string,
|
|
13
|
+
selector: string,
|
|
14
|
+
handler: (event: Event, element: Element) => void,
|
|
15
|
+
options?: DelegateOptions
|
|
16
|
+
): DelegateHandle {
|
|
17
|
+
const useCapture = options?.useCapture ?? false;
|
|
18
|
+
|
|
19
|
+
const listener = (event: Event) => {
|
|
20
|
+
let element: Element | null = event.target as Element | null;
|
|
21
|
+
|
|
22
|
+
// Handle composed events (shadow DOM)
|
|
23
|
+
if (options?.composed && typeof event.composedPath === 'function') {
|
|
24
|
+
const path = event.composedPath();
|
|
25
|
+
for (const node of path) {
|
|
26
|
+
if (node === target) break;
|
|
27
|
+
if (node instanceof Element && node.matches(selector)) {
|
|
28
|
+
handler(event, node);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
while (element && element !== target) {
|
|
36
|
+
if (element.matches(selector)) {
|
|
37
|
+
handler(event, element);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
element = element.parentElement;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
target.addEventListener(eventType, listener, useCapture);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
destroy() {
|
|
48
|
+
target.removeEventListener(eventType, listener, useCapture);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface ParsedUrl {
|
|
2
|
+
href: string;
|
|
3
|
+
protocol: string;
|
|
4
|
+
hostname: string;
|
|
5
|
+
port: string;
|
|
6
|
+
pathname: string;
|
|
7
|
+
search: string;
|
|
8
|
+
hash: string;
|
|
9
|
+
origin: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseUrl(url: string): ParsedUrl {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = new URL(url, location.href);
|
|
15
|
+
return {
|
|
16
|
+
href: parsed.href,
|
|
17
|
+
protocol: parsed.protocol,
|
|
18
|
+
hostname: parsed.hostname,
|
|
19
|
+
port: parsed.port,
|
|
20
|
+
pathname: parsed.pathname,
|
|
21
|
+
search: parsed.search,
|
|
22
|
+
hash: parsed.hash,
|
|
23
|
+
origin: parsed.origin,
|
|
24
|
+
};
|
|
25
|
+
} catch {
|
|
26
|
+
return {
|
|
27
|
+
href: url,
|
|
28
|
+
protocol: '',
|
|
29
|
+
hostname: '',
|
|
30
|
+
port: '',
|
|
31
|
+
pathname: url,
|
|
32
|
+
search: '',
|
|
33
|
+
hash: '',
|
|
34
|
+
origin: '',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const SESSION_KEY_PREFIX = 'ga4_session_';
|
|
2
|
+
|
|
3
|
+
export function isSessionExpired(key: string, timeoutMinutes: number): boolean {
|
|
4
|
+
try {
|
|
5
|
+
const stored = sessionStorage.getItem(SESSION_KEY_PREFIX + key);
|
|
6
|
+
if (!stored) return true;
|
|
7
|
+
const timestamp = parseInt(stored, 10);
|
|
8
|
+
if (isNaN(timestamp)) return true;
|
|
9
|
+
return (Date.now() - timestamp) > timeoutMinutes * 60 * 1000;
|
|
10
|
+
} catch {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function updateSessionTimestamp(key: string): void {
|
|
16
|
+
try {
|
|
17
|
+
sessionStorage.setItem(SESSION_KEY_PREFIX + key, String(Date.now()));
|
|
18
|
+
} catch {
|
|
19
|
+
// sessionStorage may be unavailable or full
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getSessionValue<T>(key: string): T | null {
|
|
24
|
+
try {
|
|
25
|
+
const stored = sessionStorage.getItem(SESSION_KEY_PREFIX + key);
|
|
26
|
+
if (stored === null) return null;
|
|
27
|
+
return JSON.parse(stored) as T;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function setSessionValue(key: string, value: unknown): void {
|
|
34
|
+
try {
|
|
35
|
+
sessionStorage.setItem(SESSION_KEY_PREFIX + key, JSON.stringify(value));
|
|
36
|
+
} catch {
|
|
37
|
+
// sessionStorage may be unavailable or full
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,34 @@
|
|
|
1
|
-
import {ga} from './ga4/index';
|
|
2
|
-
import {get} from './dataLayer';
|
|
3
|
-
|
|
4
|
-
const dataLayerHelper = { get };
|
|
5
|
-
const ga4 = ga;
|
|
6
|
-
|
|
7
|
-
export {ga4, dataLayerHelper};
|
|
1
|
+
import {ga} from './ga4/index';
|
|
2
|
+
import {get} from './dataLayer';
|
|
3
|
+
|
|
4
|
+
const dataLayerHelper = { get };
|
|
5
|
+
const ga4 = ga;
|
|
6
|
+
|
|
7
|
+
export {ga4, dataLayerHelper};
|
|
8
|
+
|
|
9
|
+
// Plugin exports
|
|
10
|
+
export { EventTracker } from './plugins/event-tracker';
|
|
11
|
+
export { OutboundLinkTracker } from './plugins/outbound-link-tracker';
|
|
12
|
+
export { OutboundFormTracker } from './plugins/outbound-form-tracker';
|
|
13
|
+
export { PageVisibilityTracker } from './plugins/page-visibility-tracker';
|
|
14
|
+
export { UrlChangeTracker } from './plugins/url-change-tracker';
|
|
15
|
+
export { ImpressionTracker } from './plugins/impression-tracker';
|
|
16
|
+
export { CleanUrlTracker } from './plugins/clean-url-tracker';
|
|
17
|
+
export { MediaQueryTracker } from './plugins/media-query-tracker';
|
|
18
|
+
|
|
19
|
+
// Type exports
|
|
20
|
+
export type {
|
|
21
|
+
GA4Plugin,
|
|
22
|
+
SendFunction,
|
|
23
|
+
EventTrackerOptions,
|
|
24
|
+
OutboundLinkTrackerOptions,
|
|
25
|
+
OutboundFormTrackerOptions,
|
|
26
|
+
PageVisibilityTrackerOptions,
|
|
27
|
+
UrlChangeTrackerOptions,
|
|
28
|
+
ImpressionTrackerOptions,
|
|
29
|
+
ImpressionElementConfig,
|
|
30
|
+
CleanUrlTrackerOptions,
|
|
31
|
+
MediaQueryTrackerOptions,
|
|
32
|
+
MediaQueryDefinition,
|
|
33
|
+
MediaQueryDefinitionItem,
|
|
34
|
+
} from './types/plugins';
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { GA4Plugin, SendFunction, CleanUrlTrackerOptions } from '../types/plugins';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes URLs before they are sent with `page_view` events.
|
|
5
|
+
*
|
|
6
|
+
* Intercepts `gtag()` calls for `config` and `page_view` events
|
|
7
|
+
* and cleans the `page_location` and `page_path` parameters
|
|
8
|
+
* (strip query params, normalize trailing slashes, apply custom filters).
|
|
9
|
+
*/
|
|
10
|
+
export class CleanUrlTracker implements GA4Plugin {
|
|
11
|
+
private opts: CleanUrlTrackerOptions;
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
13
|
+
private originalGtag: Function | null = null;
|
|
14
|
+
|
|
15
|
+
constructor(_send: SendFunction, options?: CleanUrlTrackerOptions) {
|
|
16
|
+
this.opts = {
|
|
17
|
+
stripQuery: options?.stripQuery ?? false,
|
|
18
|
+
queryParamsAllowlist: options?.queryParamsAllowlist,
|
|
19
|
+
queryParamsDenylist: options?.queryParamsDenylist,
|
|
20
|
+
trailingSlash: options?.trailingSlash,
|
|
21
|
+
urlFilter: options?.urlFilter,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (typeof window !== 'undefined' && window.gtag) {
|
|
25
|
+
this.originalGtag = window.gtag;
|
|
26
|
+
const self = this;
|
|
27
|
+
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
(window as any).gtag = function () {
|
|
30
|
+
// eslint-disable-next-line prefer-rest-params
|
|
31
|
+
const args = Array.prototype.slice.call(arguments);
|
|
32
|
+
|
|
33
|
+
if (args.length >= 3 && typeof args[2] === 'object' && args[2] !== null) {
|
|
34
|
+
const isPageView = args[0] === 'event' && args[1] === 'page_view';
|
|
35
|
+
const isConfig = args[0] === 'config';
|
|
36
|
+
|
|
37
|
+
if (isPageView || isConfig) {
|
|
38
|
+
args[2] = self.cleanParams({ ...args[2] });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return self.originalGtag!.apply(window, args);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private cleanParams(params: Record<string, unknown>): Record<string, unknown> {
|
|
48
|
+
if (typeof params.page_location === 'string') {
|
|
49
|
+
params.page_location = this.cleanUrl(params.page_location);
|
|
50
|
+
}
|
|
51
|
+
if (typeof params.page_path === 'string') {
|
|
52
|
+
params.page_path = this.cleanPath(params.page_path);
|
|
53
|
+
}
|
|
54
|
+
return params;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
cleanUrl(url: string): string {
|
|
58
|
+
try {
|
|
59
|
+
const u = new URL(url);
|
|
60
|
+
u.pathname = this.cleanPath(u.pathname);
|
|
61
|
+
|
|
62
|
+
if (this.opts.stripQuery) {
|
|
63
|
+
if (this.opts.queryParamsAllowlist && this.opts.queryParamsAllowlist.length > 0) {
|
|
64
|
+
const allowed = new URLSearchParams();
|
|
65
|
+
this.opts.queryParamsAllowlist.forEach((param) => {
|
|
66
|
+
if (u.searchParams.has(param)) {
|
|
67
|
+
allowed.set(param, u.searchParams.get(param)!);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
u.search = allowed.toString() ? '?' + allowed.toString() : '';
|
|
71
|
+
} else {
|
|
72
|
+
u.search = '';
|
|
73
|
+
}
|
|
74
|
+
} else if (this.opts.queryParamsDenylist && this.opts.queryParamsDenylist.length > 0) {
|
|
75
|
+
this.opts.queryParamsDenylist.forEach((param) => {
|
|
76
|
+
u.searchParams.delete(param);
|
|
77
|
+
});
|
|
78
|
+
u.search = u.searchParams.toString() ? '?' + u.searchParams.toString() : '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let result = u.toString();
|
|
82
|
+
if (this.opts.urlFilter) {
|
|
83
|
+
result = this.opts.urlFilter(result);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
} catch {
|
|
87
|
+
return url;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
cleanPath(path: string): string {
|
|
92
|
+
let result = path;
|
|
93
|
+
|
|
94
|
+
if (this.opts.trailingSlash === 'remove') {
|
|
95
|
+
result = result.length > 1 ? result.replace(/\/+$/, '') : result;
|
|
96
|
+
} else if (this.opts.trailingSlash === 'add') {
|
|
97
|
+
if (!result.endsWith('/') && !result.split('/').pop()?.includes('.')) {
|
|
98
|
+
result += '/';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
remove(): void {
|
|
106
|
+
if (this.originalGtag) {
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
+
(window as any).gtag = this.originalGtag;
|
|
109
|
+
this.originalGtag = null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|