@financial-times/custom-code-component 1.9.1 → 1.9.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/custom-code-component",
3
- "version": "1.9.1",
3
+ "version": "1.9.3",
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
  },
@@ -0,0 +1,137 @@
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
+ // default values if nothing is passed into init
10
+ let cccName = "ccc-component";
11
+ let subtype = "interactive";
12
+
13
+ const eventPropertiesToCollect = ["ctrlKey", "altKey", "shiftKey", "metaKey"];
14
+
15
+ // Get properties for the event (as opposed to properties of the clicked element)
16
+ // Available properties include mouse x- and y co-ordinates, for example.
17
+ const getEventProperties = (event) => {
18
+ const eventProperties = {};
19
+ for (const property of eventPropertiesToCollect) {
20
+ if (event[property]) {
21
+ try {
22
+ eventProperties[property] = sanitise(event[property]);
23
+ } catch (e) {
24
+ // eslint-disable-next-line no-console
25
+ console.log(e);
26
+ }
27
+ }
28
+ }
29
+
30
+ return eventProperties;
31
+ };
32
+
33
+ // Controller for handling click events
34
+ const handleClickEvent = (eventData, root) => (clickEvent, clickElement) => {
35
+ const context = getEventProperties(clickEvent);
36
+ const { trace, customContext } = getTrace(clickElement, root);
37
+ context.custom =
38
+ clickElement.dataset && clickElement.dataset.custom
39
+ ? JSON.parse(clickElement.dataset.custom)
40
+ : null;
41
+ context.domPathTokens = trace;
42
+ context.component = {
43
+ id: cccName,
44
+ name: cccName,
45
+ type: "custom-code-component",
46
+ subtype,
47
+ };
48
+ context.teamName = "djd";
49
+
50
+ assignIfUndefined(customContext, context);
51
+
52
+ eventData.context = context;
53
+ eventData.method = "ftCustomAnalytics";
54
+
55
+ // send spoor event
56
+ document.body.dispatchEvent(
57
+ new CustomEvent("oTracking.event", {
58
+ detail: eventData,
59
+ bubbles: true,
60
+ composed: true,
61
+ })
62
+ );
63
+ };
64
+
65
+ function sendSpoorEvent(triggerAction, extraDetail) {
66
+ const eventData = {
67
+ category: "component",
68
+ action: "act",
69
+ context: {
70
+ component: {
71
+ id: cccName,
72
+ name: cccName,
73
+ type: "custom-code-component",
74
+ subtype,
75
+ },
76
+ teamName: "djd",
77
+ trigger_action: triggerAction,
78
+ custom: extraDetail,
79
+ },
80
+ method: "ftCustomAnalytics",
81
+ };
82
+
83
+ // send spoor event
84
+ document.body.dispatchEvent(
85
+ new CustomEvent("oTracking.event", {
86
+ detail: eventData,
87
+ bubbles: true,
88
+ composed: true,
89
+ })
90
+ );
91
+ }
92
+
93
+ function init(
94
+ templateObj: { id?: string; subtype?: string },
95
+ shadowRoot?: ShadowRoot,
96
+ category?: string,
97
+ elements?: []
98
+ ) {
99
+ if (!tracking.isInitialised) {
100
+ tracking.isInitialised = true;
101
+ if (templateObj) {
102
+ cccName = templateObj.id || cccName;
103
+ subtype = templateObj.subtype || subtype;
104
+ }
105
+
106
+ oTracking.init({ queue: true, test: true }); // @TODO: Flip this to false before using in production
107
+
108
+ const elementsToTrack = elements || 'a, button, input, [role="button"]';
109
+
110
+ // Note: `context` is the term o-tracking uses for the data that is sent to spoor
111
+ const eventData = {
112
+ action: "click",
113
+ category: category || "cta",
114
+ };
115
+
116
+ const root = shadowRoot?.querySelector("#component-root");
117
+
118
+ if (shadowRoot) {
119
+ const shadowDelegate = new Delegate(root);
120
+
121
+ shadowDelegate.on(
122
+ "click",
123
+ elementsToTrack,
124
+ handleClickEvent(eventData, root),
125
+ true
126
+ );
127
+ }
128
+ }
129
+ }
130
+
131
+ const tracking = {
132
+ init,
133
+ isInitialised: false,
134
+ sendSpoorEvent,
135
+ };
136
+
137
+ export default tracking;
@@ -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 "./analytics";
9
+
8
10
  class FTCustomCodeComponent extends HTMLElement {
9
11
  app: typeof BaseRenderer.prototype.render;
10
12
 
@@ -46,7 +48,12 @@ class FTCustomCodeComponent extends HTMLElement {
46
48
  const { unmount, onmessage } =
47
49
  App(
48
50
  shadow,
49
- { ...extraProps, data, port: this.channel.port2 },
51
+ {
52
+ ...extraProps,
53
+ data,
54
+ port: this.channel.port2,
55
+ tracking,
56
+ },
50
57
  ...this.children
51
58
  ) || {};
52
59
 
@@ -153,6 +160,21 @@ class FTCustomCodeComponent extends HTMLElement {
153
160
 
154
161
  delete this.dataset.cccReady;
155
162
  }
163
+
164
+ try {
165
+ tracking.init(
166
+ {
167
+ id: this.getAttribute("path") as string,
168
+ subtype: "interactive",
169
+ },
170
+ this.shadowRoot
171
+ );
172
+ } catch (e) {
173
+ console.info(
174
+ `Error initialising tracking on <custom-code-component> ${path}@${componentVersionRange}`
175
+ );
176
+ console.error(e);
177
+ }
156
178
  }
157
179
 
158
180
  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
+ }