@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.
@@ -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.2",
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
- { ...extraProps, data, port: this.channel.port2 },
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() {
@@ -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
+ }
@@ -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;