@financial-times/custom-code-component 1.9.2 → 1.9.4
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/dist/custom-code-component.js +1238 -39
- package/dist/tracking.d.ts +22 -0
- package/package.json +3 -1
- package/src/custom-code-component.ts +26 -1
- package/src/get-trace.js +135 -0
- package/src/tracking.ts +141 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
declare class Tracking {
|
|
2
|
+
cccName: string;
|
|
3
|
+
subtype: string;
|
|
4
|
+
teamName: string;
|
|
5
|
+
shadowRoot: ShadowRoot | null;
|
|
6
|
+
category: string;
|
|
7
|
+
elements: string | string[];
|
|
8
|
+
isInitialised: boolean;
|
|
9
|
+
constructor({ id, subtype, teamName, shadowRoot, category, elements, }?: {
|
|
10
|
+
id?: string;
|
|
11
|
+
subtype?: string;
|
|
12
|
+
teamName?: string;
|
|
13
|
+
shadowRoot?: any;
|
|
14
|
+
category?: string;
|
|
15
|
+
elements?: string;
|
|
16
|
+
});
|
|
17
|
+
getEventProperties(event: any): {};
|
|
18
|
+
handleClickEvent(eventData: any, root: any): (clickEvent: any, clickElement: any) => void;
|
|
19
|
+
sendSpoorEvent(triggerAction: any, extraDetail: any): void;
|
|
20
|
+
init(): void;
|
|
21
|
+
}
|
|
22
|
+
export default Tracking;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@financial-times/custom-code-component",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
"vite-plugin-dts": "^3.6.0"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@financial-times/o-tracking": "^4.5.4",
|
|
30
|
+
"ftdomdelegate": "^5.0.0",
|
|
29
31
|
"react": "^18.2.0",
|
|
30
32
|
"react-dom": "^18.2.0"
|
|
31
33
|
},
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { ContentTree } from "@financial-times/content-tree";
|
|
7
7
|
import { BaseRenderer } from "../../ccc-sdk/src/renderers/AbstractBaseRenderer";
|
|
8
|
+
import Tracking from "./tracking";
|
|
9
|
+
|
|
8
10
|
class FTCustomCodeComponent extends HTMLElement {
|
|
9
11
|
app: typeof BaseRenderer.prototype.render;
|
|
10
12
|
|
|
@@ -22,6 +24,7 @@ class FTCustomCodeComponent extends HTMLElement {
|
|
|
22
24
|
]);
|
|
23
25
|
|
|
24
26
|
source: string;
|
|
27
|
+
tracking: Tracking;
|
|
25
28
|
|
|
26
29
|
async mount() {
|
|
27
30
|
if (!this.app) {
|
|
@@ -43,10 +46,23 @@ class FTCustomCodeComponent extends HTMLElement {
|
|
|
43
46
|
// Clear old children
|
|
44
47
|
this.shadowRoot?.replaceChildren();
|
|
45
48
|
|
|
49
|
+
// Create tracking instance
|
|
50
|
+
this.tracking = new Tracking({
|
|
51
|
+
id: `${this.getAttribute("path")}@${this.getAttribute("version")}`,
|
|
52
|
+
subtype: "interactive",
|
|
53
|
+
teamName: "djd",
|
|
54
|
+
shadowRoot: this.shadowRoot as ShadowRoot,
|
|
55
|
+
});
|
|
56
|
+
|
|
46
57
|
const { unmount, onmessage } =
|
|
47
58
|
App(
|
|
48
59
|
shadow,
|
|
49
|
-
{
|
|
60
|
+
{
|
|
61
|
+
...extraProps,
|
|
62
|
+
data,
|
|
63
|
+
port: this.channel.port2,
|
|
64
|
+
tracking: this.tracking,
|
|
65
|
+
},
|
|
50
66
|
...this.children
|
|
51
67
|
) || {};
|
|
52
68
|
|
|
@@ -153,6 +169,15 @@ class FTCustomCodeComponent extends HTMLElement {
|
|
|
153
169
|
|
|
154
170
|
delete this.dataset.cccReady;
|
|
155
171
|
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
this.tracking.init();
|
|
175
|
+
} catch (e) {
|
|
176
|
+
console.info(
|
|
177
|
+
`Error initialising tracking on <custom-code-component> ${path}@${componentVersionRange}`
|
|
178
|
+
);
|
|
179
|
+
console.error(e);
|
|
180
|
+
}
|
|
156
181
|
}
|
|
157
182
|
|
|
158
183
|
disconnectedCallback() {
|
package/src/get-trace.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Trace the element and all of its parents, collecting properties as we go
|
|
2
|
+
import {
|
|
3
|
+
sanitise,
|
|
4
|
+
assignIfUndefined,
|
|
5
|
+
} from "@financial-times/o-tracking/src/javascript/utils.js";
|
|
6
|
+
|
|
7
|
+
// For a given container element, get the number of elements that match the
|
|
8
|
+
// original element (siblings); and the index of the original element (position).
|
|
9
|
+
const getSiblingsAndPosition = (el, originalEl, selector) => {
|
|
10
|
+
const siblings = Array.from(el.querySelectorAll(selector));
|
|
11
|
+
const position = siblings.findIndex((item) => item === originalEl);
|
|
12
|
+
if (position === -1) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
siblings: siblings.length,
|
|
17
|
+
position,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const elementPropertiesToCollect = [
|
|
22
|
+
"nodeName",
|
|
23
|
+
"className",
|
|
24
|
+
"id",
|
|
25
|
+
"href",
|
|
26
|
+
"text",
|
|
27
|
+
"role",
|
|
28
|
+
];
|
|
29
|
+
// Get all (sanitised) properties of a given element.
|
|
30
|
+
const getAllElementProperties = (element) => {
|
|
31
|
+
const properties = {};
|
|
32
|
+
for (const property of elementPropertiesToCollect) {
|
|
33
|
+
const value =
|
|
34
|
+
element[property] ||
|
|
35
|
+
element.getAttribute(property) ||
|
|
36
|
+
element.hasAttribute(property);
|
|
37
|
+
if (value !== undefined) {
|
|
38
|
+
if (typeof value === "boolean") {
|
|
39
|
+
properties[property] = value;
|
|
40
|
+
} else {
|
|
41
|
+
properties[property] = sanitise(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return properties;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const parseRawValue = (rawValue) => {
|
|
50
|
+
try {
|
|
51
|
+
const parsedValue = JSON.parse(rawValue);
|
|
52
|
+
const type = Object.prototype.toString.call(parsedValue);
|
|
53
|
+
const isJSON = type === "[object Object]" || type === "[object Array]";
|
|
54
|
+
|
|
55
|
+
return [isJSON, parsedValue];
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return [false, null];
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const getAttributeValue = (rawValue) => {
|
|
62
|
+
const [isJSON, value] = parseRawValue(rawValue);
|
|
63
|
+
|
|
64
|
+
return isJSON ? value : rawValue;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Get some properties of a given element.
|
|
68
|
+
const getDomPathProps = (attrs, props) => {
|
|
69
|
+
// Collect any attribute that matches given strings.
|
|
70
|
+
attrs
|
|
71
|
+
.filter((attribute) =>
|
|
72
|
+
attribute.name.match(/^data-trackable|^data-o-|^aria-/i)
|
|
73
|
+
)
|
|
74
|
+
.forEach((attribute) => {
|
|
75
|
+
props[attribute.name] = attribute.value;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return props;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Get only the custom data-trackable-context-? properties of a given element
|
|
82
|
+
const getContextProps = (attrs, props, isOriginalEl) => {
|
|
83
|
+
const customProps = {};
|
|
84
|
+
|
|
85
|
+
// for the original element collect properties like className, nodeName
|
|
86
|
+
if (isOriginalEl) {
|
|
87
|
+
elementPropertiesToCollect.forEach((name) => {
|
|
88
|
+
if (typeof props[name] !== "undefined" && name !== "id") {
|
|
89
|
+
customProps[name] = props[name];
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Collect any attribute that matches given strings.
|
|
95
|
+
attrs
|
|
96
|
+
.filter((attribute) => attribute.name.match(/^data-trackable-context-/i))
|
|
97
|
+
.forEach((attribute) => {
|
|
98
|
+
customProps[attribute.name.replace("data-trackable-context-", "")] =
|
|
99
|
+
getAttributeValue(attribute.value);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return customProps;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export function getTrace(el, rootEl) {
|
|
106
|
+
const originalEl = el;
|
|
107
|
+
const selector = originalEl.getAttribute("data-trackable")
|
|
108
|
+
? `[data-trackable="${originalEl.getAttribute("data-trackable")}"]`
|
|
109
|
+
: originalEl.nodeName;
|
|
110
|
+
const trace = [];
|
|
111
|
+
const customContext = {};
|
|
112
|
+
while (el && el !== rootEl) {
|
|
113
|
+
const props = getAllElementProperties(el);
|
|
114
|
+
const attrs = Array.from(el.attributes);
|
|
115
|
+
let domPathProps = getDomPathProps(attrs, props);
|
|
116
|
+
|
|
117
|
+
// If the element happens to have a data-trackable attribute, get the siblings
|
|
118
|
+
// and position of the element (relative to the current element).
|
|
119
|
+
if (domPathProps["data-trackable"]) {
|
|
120
|
+
domPathProps = Object.assign(
|
|
121
|
+
domPathProps,
|
|
122
|
+
getSiblingsAndPosition(el, originalEl, selector)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
trace.push(domPathProps);
|
|
127
|
+
|
|
128
|
+
const contextProps = getContextProps(attrs, props, el === originalEl);
|
|
129
|
+
|
|
130
|
+
assignIfUndefined(contextProps, customContext);
|
|
131
|
+
|
|
132
|
+
el = el.parentNode;
|
|
133
|
+
}
|
|
134
|
+
return { trace, customContext };
|
|
135
|
+
}
|
package/src/tracking.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import Delegate from "ftdomdelegate";
|
|
2
|
+
import oTracking from "@financial-times/o-tracking";
|
|
3
|
+
import { getTrace } from "./get-trace";
|
|
4
|
+
import {
|
|
5
|
+
sanitise,
|
|
6
|
+
assignIfUndefined,
|
|
7
|
+
} from "@financial-times/o-tracking/src/javascript/utils.js";
|
|
8
|
+
|
|
9
|
+
const eventPropertiesToCollect = ["ctrlKey", "altKey", "shiftKey", "metaKey"];
|
|
10
|
+
|
|
11
|
+
class Tracking {
|
|
12
|
+
cccName: string;
|
|
13
|
+
subtype: string;
|
|
14
|
+
teamName: string;
|
|
15
|
+
shadowRoot: ShadowRoot | null;
|
|
16
|
+
category: string;
|
|
17
|
+
elements: string | string[];
|
|
18
|
+
isInitialised: boolean;
|
|
19
|
+
|
|
20
|
+
constructor({
|
|
21
|
+
id = "ccc-component",
|
|
22
|
+
subtype = "interactive",
|
|
23
|
+
teamName = "djd",
|
|
24
|
+
shadowRoot = null,
|
|
25
|
+
category = "cta",
|
|
26
|
+
elements = 'a, button, input, [role="button"]',
|
|
27
|
+
} = {}) {
|
|
28
|
+
this.cccName = id;
|
|
29
|
+
this.subtype = subtype;
|
|
30
|
+
this.teamName = teamName;
|
|
31
|
+
this.shadowRoot = shadowRoot;
|
|
32
|
+
this.category = category;
|
|
33
|
+
this.elements = elements;
|
|
34
|
+
this.isInitialised = false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Get properties for the event (as opposed to properties of the clicked element)
|
|
38
|
+
getEventProperties(event) {
|
|
39
|
+
const eventProperties = {};
|
|
40
|
+
for (const property of eventPropertiesToCollect) {
|
|
41
|
+
if (event[property]) {
|
|
42
|
+
try {
|
|
43
|
+
eventProperties[property] = sanitise(event[property]);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.log(e);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return eventProperties;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Controller for handling click events
|
|
53
|
+
handleClickEvent(eventData, root) {
|
|
54
|
+
return (clickEvent, clickElement) => {
|
|
55
|
+
const context: any = this.getEventProperties(clickEvent);
|
|
56
|
+
const { trace, customContext } = getTrace(clickElement, root);
|
|
57
|
+
context.custom =
|
|
58
|
+
clickElement.dataset && clickElement.dataset.custom
|
|
59
|
+
? JSON.parse(clickElement.dataset.custom)
|
|
60
|
+
: null;
|
|
61
|
+
context.domPathTokens = trace;
|
|
62
|
+
context.component = {
|
|
63
|
+
id: this.cccName,
|
|
64
|
+
name: this.cccName,
|
|
65
|
+
type: "custom-code-component",
|
|
66
|
+
subtype: this.subtype,
|
|
67
|
+
};
|
|
68
|
+
context.teamName = this.teamName;
|
|
69
|
+
|
|
70
|
+
assignIfUndefined(customContext, context);
|
|
71
|
+
|
|
72
|
+
eventData.context = context;
|
|
73
|
+
eventData.method = "ftCustomAnalytics";
|
|
74
|
+
|
|
75
|
+
// send spoor event
|
|
76
|
+
document.body.dispatchEvent(
|
|
77
|
+
new CustomEvent("oTracking.event", {
|
|
78
|
+
detail: eventData,
|
|
79
|
+
bubbles: true,
|
|
80
|
+
composed: true,
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
sendSpoorEvent(triggerAction, extraDetail) {
|
|
87
|
+
const eventData = {
|
|
88
|
+
category: "component",
|
|
89
|
+
action: "act",
|
|
90
|
+
context: {
|
|
91
|
+
component: {
|
|
92
|
+
id: this.cccName,
|
|
93
|
+
name: this.cccName,
|
|
94
|
+
type: "custom-code-component",
|
|
95
|
+
subtype: this.subtype,
|
|
96
|
+
},
|
|
97
|
+
teamName: this.teamName,
|
|
98
|
+
trigger_action: triggerAction,
|
|
99
|
+
custom: extraDetail,
|
|
100
|
+
},
|
|
101
|
+
method: "ftCustomAnalytics",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// send spoor event
|
|
105
|
+
document.body.dispatchEvent(
|
|
106
|
+
new CustomEvent("oTracking.event", {
|
|
107
|
+
detail: eventData,
|
|
108
|
+
bubbles: true,
|
|
109
|
+
composed: true,
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
init() {
|
|
115
|
+
if (!this.isInitialised) {
|
|
116
|
+
this.isInitialised = true;
|
|
117
|
+
|
|
118
|
+
oTracking.init({ queue: true, test: true }); // @TODO: Flip this to false before using in production
|
|
119
|
+
|
|
120
|
+
const eventData = {
|
|
121
|
+
action: "click",
|
|
122
|
+
category: this.category,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const root = this.shadowRoot?.querySelector("#component-root");
|
|
126
|
+
|
|
127
|
+
if (this.shadowRoot) {
|
|
128
|
+
const shadowDelegate = new Delegate(root);
|
|
129
|
+
|
|
130
|
+
shadowDelegate.on(
|
|
131
|
+
"click",
|
|
132
|
+
this.elements,
|
|
133
|
+
this.handleClickEvent(eventData, root),
|
|
134
|
+
true
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default Tracking;
|