@dotcms/events 0.0.0-stage → 26.10.5-1-alpha.0
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/README.md +516 -2
- package/decision.cjs.js +108 -0
- package/decision.esm.js +92 -0
- package/index.cjs.default.js +1 -0
- package/index.cjs.js +3674 -0
- package/index.cjs.mjs +2 -0
- package/index.d.ts +1 -0
- package/index.esm.js +3672 -0
- package/markup.cjs.default.js +1 -0
- package/markup.cjs.js +161 -0
- package/markup.cjs.mjs +2 -0
- package/markup.d.ts +1 -0
- package/markup.esm.js +159 -0
- package/package.json +77 -4
- package/react.cjs.default.js +1 -0
- package/react.cjs.js +39 -0
- package/react.cjs.mjs +2 -0
- package/react.d.ts +1 -0
- package/react.esm.js +37 -0
- package/src/index.d.ts +8 -0
- package/src/lib/clicks/constants.d.ts +14 -0
- package/src/lib/clicks/plugin.d.ts +35 -0
- package/src/lib/clicks/tracker.d.ts +42 -0
- package/src/lib/clicks/utils.d.ts +13 -0
- package/src/lib/contentlets/constants.d.ts +36 -0
- package/src/lib/contentlets/utils.d.ts +64 -0
- package/src/lib/contentlets/viewport.d.ts +23 -0
- package/src/lib/events.d.ts +17 -0
- package/src/lib/experiments/api.d.ts +9 -0
- package/src/lib/experiments/boot.d.ts +53 -0
- package/src/lib/experiments/constants.d.ts +46 -0
- package/src/lib/experiments/decision.d.ts +53 -0
- package/src/lib/experiments/dom.d.ts +62 -0
- package/src/lib/experiments/engine.d.ts +66 -0
- package/src/lib/experiments/markup.d.ts +17 -0
- package/src/lib/experiments/models.d.ts +62 -0
- package/src/lib/experiments/plugin.d.ts +27 -0
- package/src/lib/experiments/store.d.ts +54 -0
- package/src/lib/impressions/constants.d.ts +28 -0
- package/src/lib/impressions/plugin.d.ts +38 -0
- package/src/lib/impressions/tracker.d.ts +67 -0
- package/src/lib/impressions/utils.d.ts +10 -0
- package/src/lib/models.d.ts +130 -0
- package/src/lib/pipeline/constants.d.ts +88 -0
- package/src/lib/pipeline/enricher/plugin.d.ts +32 -0
- package/src/lib/pipeline/identity/activity.d.ts +19 -0
- package/src/lib/pipeline/identity/plugin.d.ts +60 -0
- package/src/lib/pipeline/logger.d.ts +74 -0
- package/src/lib/pipeline/models.d.ts +588 -0
- package/src/lib/pipeline/navigation.d.ts +21 -0
- package/src/lib/pipeline/sender/http.d.ts +9 -0
- package/src/lib/pipeline/sender/plugin.d.ts +47 -0
- package/src/lib/pipeline/sender/queue.d.ts +33 -0
- package/src/lib/pipeline/utils.d.ts +119 -0
- package/src/lib/react/DotCMSExperiment.d.ts +36 -0
- package/src/markup.d.ts +7 -0
- package/src/react.d.ts +8 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
exports._default = require('./markup.cjs.js').default;
|
package/markup.cjs.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var decision = require('./decision.cjs.js');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Applies, while the HTML is parsed and before the app's scripts load, what `decide` says from
|
|
7
|
+
* what the engine stored on an earlier page: it shows the content, or stops the page and
|
|
8
|
+
* replaces it with the assigned variant. It never calls dotCMS: when nothing is stored, the
|
|
9
|
+
* content stays hidden and the engine decides once it loads.
|
|
10
|
+
*
|
|
11
|
+
* It is printed into the page with `Function.prototype.toString`, next to `decideVariant`, so
|
|
12
|
+
* it must not reference anything outside itself: the decision arrives as `decide`, and every
|
|
13
|
+
* value in `config`.
|
|
14
|
+
*
|
|
15
|
+
* @param decide - `decideVariant`, the decision the engine makes too
|
|
16
|
+
* @param config - The page's experiment and the shared names
|
|
17
|
+
* @param w - The window, a parameter so tests can pass their own
|
|
18
|
+
*/
|
|
19
|
+
function dotcmsExperimentBoot(decide, config, w = window) {
|
|
20
|
+
const reveal = () => {
|
|
21
|
+
const doc = w.document;
|
|
22
|
+
const Sheet = w.CSSStyleSheet;
|
|
23
|
+
// An adopted style sheet where the browser has them: no Content-Security-Policy blocks
|
|
24
|
+
// a change through the CSSOM, while it blocks a style element without the page's nonce
|
|
25
|
+
if ('adoptedStyleSheets' in doc && typeof Sheet === 'function') {
|
|
26
|
+
try {
|
|
27
|
+
const sheet = new Sheet();
|
|
28
|
+
const sheets = Array.prototype.slice.call(doc.adoptedStyleSheets);
|
|
29
|
+
sheet.replaceSync(config.revealRule);
|
|
30
|
+
sheets.push(sheet);
|
|
31
|
+
doc.adoptedStyleSheets = sheets;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// A style element instead
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
let style = doc.getElementById(config.styleId);
|
|
39
|
+
if (!style) {
|
|
40
|
+
style = doc.createElement('style');
|
|
41
|
+
style.id = config.styleId;
|
|
42
|
+
doc.head.appendChild(style);
|
|
43
|
+
}
|
|
44
|
+
style.appendChild(doc.createTextNode(config.revealRule));
|
|
45
|
+
};
|
|
46
|
+
try {
|
|
47
|
+
// Inside the UVE editor nothing is decided and nothing stays hidden
|
|
48
|
+
if (w.parent !== w) {
|
|
49
|
+
reveal();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const read = (key) => {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(w.localStorage.getItem(key) || 'null');
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const decision = decide({
|
|
62
|
+
experimentId: config.id,
|
|
63
|
+
rendered: config.rendered,
|
|
64
|
+
href: w.location.href,
|
|
65
|
+
stored: read(config.assignmentsKey),
|
|
66
|
+
disabled: Number(read(config.disabledUntilKey) || 0) > now,
|
|
67
|
+
now,
|
|
68
|
+
param: config.param,
|
|
69
|
+
defaultVariant: config.defaultVariant
|
|
70
|
+
});
|
|
71
|
+
// A new visitor: the engine asks dotCMS once it loads
|
|
72
|
+
if (decision.kind === 'unknown') {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (decision.kind !== 'redirect') {
|
|
76
|
+
reveal();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const state = { redirectedTo: decision.url };
|
|
80
|
+
w[config.bootKey] = state;
|
|
81
|
+
// No more parsing, scripts or images on the page being replaced. It still runs until
|
|
82
|
+
// the variant's document arrives: its new requests are held, except keepalive ones (an
|
|
83
|
+
// analytics flush). A held request never settles; after quietMs, fetch works again
|
|
84
|
+
// for new calls only.
|
|
85
|
+
w.stop();
|
|
86
|
+
w.location.replace(decision.url);
|
|
87
|
+
const send = w.fetch;
|
|
88
|
+
w.fetch = (input, init) => init && init.keepalive
|
|
89
|
+
? send.call(w, input, init)
|
|
90
|
+
: new Promise(() => undefined);
|
|
91
|
+
w.setTimeout(() => {
|
|
92
|
+
w.fetch = send;
|
|
93
|
+
}, config.quietMs);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Storage blocked: the engine decides when it loads
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Escapes a value for a CSS attribute selector in double quotes. */
|
|
100
|
+
const escapeAttribute = (value) => value.replace(/["\\]/g, '\\$&');
|
|
101
|
+
/**
|
|
102
|
+
* Builds the configuration the boot script runs with.
|
|
103
|
+
*
|
|
104
|
+
* @param options - The experiment and the variant the server rendered
|
|
105
|
+
* @returns The configuration
|
|
106
|
+
*/
|
|
107
|
+
const createExperimentBootConfig = ({ experimentId, variant }) => ({
|
|
108
|
+
id: experimentId,
|
|
109
|
+
rendered: variant,
|
|
110
|
+
assignmentsKey: decision.STORAGE_KEYS.assignments,
|
|
111
|
+
disabledUntilKey: decision.STORAGE_KEYS.disabledUntil,
|
|
112
|
+
bootKey: decision.BOOT_STATE_KEY,
|
|
113
|
+
styleId: decision.REVEAL_STYLE_ID,
|
|
114
|
+
// Keyed by the rendered variant too: a route kept in the page with another render stays hidden
|
|
115
|
+
revealRule: `:root [${decision.EXPERIMENT_ATTRIBUTE}="${escapeAttribute(experimentId)}"][${decision.VARIANT_ATTRIBUTE}="${escapeAttribute(variant)}"]{visibility:visible !important;animation:none !important}`,
|
|
116
|
+
param: decision.VARIANT_QUERY_PARAM,
|
|
117
|
+
defaultVariant: decision.DEFAULT_VARIANT,
|
|
118
|
+
quietMs: decision.QUIET_MS
|
|
119
|
+
});
|
|
120
|
+
/**
|
|
121
|
+
* Builds the inline script printed before an experiment's content, right after the rule
|
|
122
|
+
* that hides it: the boot function, called with the decision function and its config.
|
|
123
|
+
*
|
|
124
|
+
* @param options - The experiment and the variant the server rendered
|
|
125
|
+
* @returns The script's source
|
|
126
|
+
*/
|
|
127
|
+
const buildExperimentBootScript = (options) => {
|
|
128
|
+
// `<` is escaped so no value can close the script element
|
|
129
|
+
const json = JSON.stringify(createExperimentBootConfig(options)).replace(/</g, '\\u003c');
|
|
130
|
+
return `(${dotcmsExperimentBoot.toString()})(${decision.decideVariant.toString()}, ${json});`;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Builds what a page prints so its experiment runs: the attributes for the element that wraps
|
|
135
|
+
* what the experiment varies, the rule that keeps it hidden, and the boot script that decides
|
|
136
|
+
* the visitor's variant while the HTML is parsed. Render it on the server, with the style and
|
|
137
|
+
* the script before the element. `DotCMSExperiment` (`@dotcms/events/react`) does it for React.
|
|
138
|
+
*
|
|
139
|
+
* @param page - The page asset, requested for the variant in the URL's `variantName`
|
|
140
|
+
* @returns The markup, or null when no experiment runs on the page
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* const markup = experimentMarkup(pageAsset);
|
|
145
|
+
* // <style>{markup.style}</style><script>{markup.script}</script><div {...markup.attributes}>…</div>
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
const experimentMarkup = (page) => {
|
|
149
|
+
const experimentId = page?.runningExperimentId;
|
|
150
|
+
if (!experimentId) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const variant = page?.viewAs?.variantId || decision.DEFAULT_VARIANT;
|
|
154
|
+
return {
|
|
155
|
+
attributes: { [decision.EXPERIMENT_ATTRIBUTE]: experimentId, [decision.VARIANT_ATTRIBUTE]: variant },
|
|
156
|
+
style: decision.HIDING_RULE,
|
|
157
|
+
script: buildExperimentBootScript({ experimentId, variant })
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
exports.experimentMarkup = experimentMarkup;
|
package/markup.cjs.mjs
ADDED
package/markup.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/markup";
|
package/markup.esm.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { d as decideVariant, S as STORAGE_KEYS, B as BOOT_STATE_KEY, R as REVEAL_STYLE_ID, E as EXPERIMENT_ATTRIBUTE, V as VARIANT_ATTRIBUTE, b as VARIANT_QUERY_PARAM, D as DEFAULT_VARIANT, Q as QUIET_MS, H as HIDING_RULE } from './decision.esm.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Applies, while the HTML is parsed and before the app's scripts load, what `decide` says from
|
|
5
|
+
* what the engine stored on an earlier page: it shows the content, or stops the page and
|
|
6
|
+
* replaces it with the assigned variant. It never calls dotCMS: when nothing is stored, the
|
|
7
|
+
* content stays hidden and the engine decides once it loads.
|
|
8
|
+
*
|
|
9
|
+
* It is printed into the page with `Function.prototype.toString`, next to `decideVariant`, so
|
|
10
|
+
* it must not reference anything outside itself: the decision arrives as `decide`, and every
|
|
11
|
+
* value in `config`.
|
|
12
|
+
*
|
|
13
|
+
* @param decide - `decideVariant`, the decision the engine makes too
|
|
14
|
+
* @param config - The page's experiment and the shared names
|
|
15
|
+
* @param w - The window, a parameter so tests can pass their own
|
|
16
|
+
*/
|
|
17
|
+
function dotcmsExperimentBoot(decide, config, w = window) {
|
|
18
|
+
const reveal = () => {
|
|
19
|
+
const doc = w.document;
|
|
20
|
+
const Sheet = w.CSSStyleSheet;
|
|
21
|
+
// An adopted style sheet where the browser has them: no Content-Security-Policy blocks
|
|
22
|
+
// a change through the CSSOM, while it blocks a style element without the page's nonce
|
|
23
|
+
if ('adoptedStyleSheets' in doc && typeof Sheet === 'function') {
|
|
24
|
+
try {
|
|
25
|
+
const sheet = new Sheet();
|
|
26
|
+
const sheets = Array.prototype.slice.call(doc.adoptedStyleSheets);
|
|
27
|
+
sheet.replaceSync(config.revealRule);
|
|
28
|
+
sheets.push(sheet);
|
|
29
|
+
doc.adoptedStyleSheets = sheets;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// A style element instead
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
let style = doc.getElementById(config.styleId);
|
|
37
|
+
if (!style) {
|
|
38
|
+
style = doc.createElement('style');
|
|
39
|
+
style.id = config.styleId;
|
|
40
|
+
doc.head.appendChild(style);
|
|
41
|
+
}
|
|
42
|
+
style.appendChild(doc.createTextNode(config.revealRule));
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
// Inside the UVE editor nothing is decided and nothing stays hidden
|
|
46
|
+
if (w.parent !== w) {
|
|
47
|
+
reveal();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const read = (key) => {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(w.localStorage.getItem(key) || 'null');
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const decision = decide({
|
|
60
|
+
experimentId: config.id,
|
|
61
|
+
rendered: config.rendered,
|
|
62
|
+
href: w.location.href,
|
|
63
|
+
stored: read(config.assignmentsKey),
|
|
64
|
+
disabled: Number(read(config.disabledUntilKey) || 0) > now,
|
|
65
|
+
now,
|
|
66
|
+
param: config.param,
|
|
67
|
+
defaultVariant: config.defaultVariant
|
|
68
|
+
});
|
|
69
|
+
// A new visitor: the engine asks dotCMS once it loads
|
|
70
|
+
if (decision.kind === 'unknown') {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (decision.kind !== 'redirect') {
|
|
74
|
+
reveal();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const state = { redirectedTo: decision.url };
|
|
78
|
+
w[config.bootKey] = state;
|
|
79
|
+
// No more parsing, scripts or images on the page being replaced. It still runs until
|
|
80
|
+
// the variant's document arrives: its new requests are held, except keepalive ones (an
|
|
81
|
+
// analytics flush). A held request never settles; after quietMs, fetch works again
|
|
82
|
+
// for new calls only.
|
|
83
|
+
w.stop();
|
|
84
|
+
w.location.replace(decision.url);
|
|
85
|
+
const send = w.fetch;
|
|
86
|
+
w.fetch = (input, init) => init && init.keepalive
|
|
87
|
+
? send.call(w, input, init)
|
|
88
|
+
: new Promise(() => undefined);
|
|
89
|
+
w.setTimeout(() => {
|
|
90
|
+
w.fetch = send;
|
|
91
|
+
}, config.quietMs);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Storage blocked: the engine decides when it loads
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Escapes a value for a CSS attribute selector in double quotes. */
|
|
98
|
+
const escapeAttribute = (value) => value.replace(/["\\]/g, '\\$&');
|
|
99
|
+
/**
|
|
100
|
+
* Builds the configuration the boot script runs with.
|
|
101
|
+
*
|
|
102
|
+
* @param options - The experiment and the variant the server rendered
|
|
103
|
+
* @returns The configuration
|
|
104
|
+
*/
|
|
105
|
+
const createExperimentBootConfig = ({ experimentId, variant }) => ({
|
|
106
|
+
id: experimentId,
|
|
107
|
+
rendered: variant,
|
|
108
|
+
assignmentsKey: STORAGE_KEYS.assignments,
|
|
109
|
+
disabledUntilKey: STORAGE_KEYS.disabledUntil,
|
|
110
|
+
bootKey: BOOT_STATE_KEY,
|
|
111
|
+
styleId: REVEAL_STYLE_ID,
|
|
112
|
+
// Keyed by the rendered variant too: a route kept in the page with another render stays hidden
|
|
113
|
+
revealRule: `:root [${EXPERIMENT_ATTRIBUTE}="${escapeAttribute(experimentId)}"][${VARIANT_ATTRIBUTE}="${escapeAttribute(variant)}"]{visibility:visible !important;animation:none !important}`,
|
|
114
|
+
param: VARIANT_QUERY_PARAM,
|
|
115
|
+
defaultVariant: DEFAULT_VARIANT,
|
|
116
|
+
quietMs: QUIET_MS
|
|
117
|
+
});
|
|
118
|
+
/**
|
|
119
|
+
* Builds the inline script printed before an experiment's content, right after the rule
|
|
120
|
+
* that hides it: the boot function, called with the decision function and its config.
|
|
121
|
+
*
|
|
122
|
+
* @param options - The experiment and the variant the server rendered
|
|
123
|
+
* @returns The script's source
|
|
124
|
+
*/
|
|
125
|
+
const buildExperimentBootScript = (options) => {
|
|
126
|
+
// `<` is escaped so no value can close the script element
|
|
127
|
+
const json = JSON.stringify(createExperimentBootConfig(options)).replace(/</g, '\\u003c');
|
|
128
|
+
return `(${dotcmsExperimentBoot.toString()})(${decideVariant.toString()}, ${json});`;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Builds what a page prints so its experiment runs: the attributes for the element that wraps
|
|
133
|
+
* what the experiment varies, the rule that keeps it hidden, and the boot script that decides
|
|
134
|
+
* the visitor's variant while the HTML is parsed. Render it on the server, with the style and
|
|
135
|
+
* the script before the element. `DotCMSExperiment` (`@dotcms/events/react`) does it for React.
|
|
136
|
+
*
|
|
137
|
+
* @param page - The page asset, requested for the variant in the URL's `variantName`
|
|
138
|
+
* @returns The markup, or null when no experiment runs on the page
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* const markup = experimentMarkup(pageAsset);
|
|
143
|
+
* // <style>{markup.style}</style><script>{markup.script}</script><div {...markup.attributes}>…</div>
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
const experimentMarkup = (page) => {
|
|
147
|
+
const experimentId = page?.runningExperimentId;
|
|
148
|
+
if (!experimentId) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const variant = page?.viewAs?.variantId || DEFAULT_VARIANT;
|
|
152
|
+
return {
|
|
153
|
+
attributes: { [EXPERIMENT_ATTRIBUTE]: experimentId, [VARIANT_ATTRIBUTE]: variant },
|
|
154
|
+
style: HIDING_RULE,
|
|
155
|
+
script: buildExperimentBootScript({ experimentId, variant })
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export { experimentMarkup };
|
package/package.json
CHANGED
|
@@ -1,6 +1,79 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotcms/events",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "26.10.5-1-alpha.0",
|
|
4
|
+
"description": "Official JavaScript library for dotCMS events: pageviews, conversions, content clicks and impressions, and experiments.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/dotCMS/core.git#main"
|
|
8
|
+
},
|
|
9
|
+
"peerDependencies": {
|
|
10
|
+
"@dotcms/uve": "26.10.5-1",
|
|
11
|
+
"react": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"peerDependenciesMeta": {
|
|
14
|
+
"react": {
|
|
15
|
+
"optional": true
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@analytics/queue-utils": "^0.1.3",
|
|
20
|
+
"@analytics/router-utils": "^0.1.1",
|
|
21
|
+
"analytics": "^0.8.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@dotcms/types": "26.10.5-1"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"dotCMS",
|
|
28
|
+
"CMS",
|
|
29
|
+
"Content Management",
|
|
30
|
+
"Analytics",
|
|
31
|
+
"Experiments",
|
|
32
|
+
"Events",
|
|
33
|
+
"Tracking"
|
|
34
|
+
],
|
|
35
|
+
"exports": {
|
|
36
|
+
"./package.json": "./package.json",
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./index.d.ts",
|
|
39
|
+
"module": "./index.esm.js",
|
|
40
|
+
"import": "./index.cjs.mjs",
|
|
41
|
+
"default": "./index.cjs.js"
|
|
42
|
+
},
|
|
43
|
+
"./markup": {
|
|
44
|
+
"types": "./markup.d.ts",
|
|
45
|
+
"module": "./markup.esm.js",
|
|
46
|
+
"import": "./markup.cjs.mjs",
|
|
47
|
+
"default": "./markup.cjs.js"
|
|
48
|
+
},
|
|
49
|
+
"./react": {
|
|
50
|
+
"types": "./react.d.ts",
|
|
51
|
+
"module": "./react.esm.js",
|
|
52
|
+
"import": "./react.cjs.mjs",
|
|
53
|
+
"default": "./react.cjs.js"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"typesVersions": {
|
|
57
|
+
"*": {
|
|
58
|
+
".": [
|
|
59
|
+
"./src/index.d.ts"
|
|
60
|
+
],
|
|
61
|
+
"markup": [
|
|
62
|
+
"./src/markup.d.ts"
|
|
63
|
+
],
|
|
64
|
+
"react": [
|
|
65
|
+
"./src/react.d.ts"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"author": "dotcms <dev@dotcms.com>",
|
|
70
|
+
"license": "MIT",
|
|
71
|
+
"bugs": {
|
|
72
|
+
"url": "https://github.com/dotCMS/core/issues"
|
|
73
|
+
},
|
|
74
|
+
"homepage": "https://github.com/dotCMS/core/tree/main/core-web/libs/sdk/events/README.md",
|
|
75
|
+
"sideEffects": false,
|
|
76
|
+
"module": "./index.esm.js",
|
|
77
|
+
"main": "./index.cjs.js",
|
|
78
|
+
"types": "./index.d.ts"
|
|
79
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
exports._default = require('./react.cjs.js').default;
|
package/react.cjs.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
+
var markup = require('./markup.cjs.js');
|
|
5
|
+
require('./decision.cjs.js');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Runs the page's experiment on its content. On a page that runs an experiment, it wraps the
|
|
9
|
+
* content in an element marked with the experiment and the variant the server rendered, keeps
|
|
10
|
+
* it hidden, and prints the script that decides the visitor's variant while the HTML loads.
|
|
11
|
+
* `dotEvents` then shows the content, or the page is replaced with the assigned variant. On any
|
|
12
|
+
* other page it renders the content alone.
|
|
13
|
+
*
|
|
14
|
+
* It has no hooks, so it also renders in a server component, which keeps the script builder
|
|
15
|
+
* out of the client bundle.
|
|
16
|
+
*
|
|
17
|
+
* @param props - The page asset, an optional nonce and class, and the content
|
|
18
|
+
* @returns The content, marked and hidden until its variant is decided on an experiment page
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <DotCMSExperiment page={pageAsset}>
|
|
23
|
+
* <DotCMSLayoutBody page={pageAsset} components={pageComponents} />
|
|
24
|
+
* </DotCMSExperiment>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
const DotCMSExperiment = ({ page, nonce, className, children }) => {
|
|
28
|
+
const markup$1 = markup.experimentMarkup(page);
|
|
29
|
+
// A fragment, so the component always returns an element: its type reads plainly, and
|
|
30
|
+
// JSX accepts it with every @types/react version
|
|
31
|
+
if (!markup$1) {
|
|
32
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
|
|
33
|
+
}
|
|
34
|
+
// Browsers hide a nonce from the DOM once they apply it, which hydration reads as a mismatch
|
|
35
|
+
const nonceProps = nonce ? { nonce, suppressHydrationWarning: true } : {};
|
|
36
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("style", { ...nonceProps, children: markup$1.style }), jsxRuntime.jsx("script", { ...nonceProps, dangerouslySetInnerHTML: { __html: markup$1.script } }), jsxRuntime.jsx("div", { className: className, ...markup$1.attributes, children: children })] }));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
exports.DotCMSExperiment = DotCMSExperiment;
|
package/react.cjs.mjs
ADDED
package/react.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/react";
|
package/react.esm.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import { experimentMarkup } from './markup.esm.js';
|
|
3
|
+
import './decision.esm.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runs the page's experiment on its content. On a page that runs an experiment, it wraps the
|
|
7
|
+
* content in an element marked with the experiment and the variant the server rendered, keeps
|
|
8
|
+
* it hidden, and prints the script that decides the visitor's variant while the HTML loads.
|
|
9
|
+
* `dotEvents` then shows the content, or the page is replaced with the assigned variant. On any
|
|
10
|
+
* other page it renders the content alone.
|
|
11
|
+
*
|
|
12
|
+
* It has no hooks, so it also renders in a server component, which keeps the script builder
|
|
13
|
+
* out of the client bundle.
|
|
14
|
+
*
|
|
15
|
+
* @param props - The page asset, an optional nonce and class, and the content
|
|
16
|
+
* @returns The content, marked and hidden until its variant is decided on an experiment page
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <DotCMSExperiment page={pageAsset}>
|
|
21
|
+
* <DotCMSLayoutBody page={pageAsset} components={pageComponents} />
|
|
22
|
+
* </DotCMSExperiment>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
const DotCMSExperiment = ({ page, nonce, className, children }) => {
|
|
26
|
+
const markup = experimentMarkup(page);
|
|
27
|
+
// A fragment, so the component always returns an element: its type reads plainly, and
|
|
28
|
+
// JSX accepts it with every @types/react version
|
|
29
|
+
if (!markup) {
|
|
30
|
+
return jsx(Fragment, { children: children });
|
|
31
|
+
}
|
|
32
|
+
// Browsers hide a nonce from the DOM once they apply it, which hydration reads as a mismatch
|
|
33
|
+
const nonceProps = nonce ? { nonce, suppressHydrationWarning: true } : {};
|
|
34
|
+
return (jsxs(Fragment, { children: [jsx("style", { ...nonceProps, children: markup.style }), jsx("script", { ...nonceProps, dangerouslySetInnerHTML: { __html: markup.script } }), jsx("div", { className: className, ...markup.attributes, children: children })] }));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { DotCMSExperiment };
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dotcms/events`: the `dotEvents` object and the types its methods and options use.
|
|
3
|
+
*
|
|
4
|
+
* Every page loads this entry, so it holds the SDK and no markup: what a page prints so its
|
|
5
|
+
* experiment runs comes from `./markup` and `./react`.
|
|
6
|
+
*/
|
|
7
|
+
export { dotEvents } from './lib/events';
|
|
8
|
+
export type { DotCMSEvents, DotCMSEventsConfig, DotCMSEventsError, DotCMSEventsErrorCode, DotCMSEventsExperimentsConfig, DotCMSEventsImpressionsConfig, DotCMSEventsJsonObject, DotCMSEventsJsonValue, DotCMSEventsLogLevel, DotCMSEventsQueueConfig } from './lib/models';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event type for content clicks
|
|
3
|
+
* Must match DotCMSPredefinedEventType.CONTENT_CLICK
|
|
4
|
+
*/
|
|
5
|
+
export declare const CLICK_EVENT_TYPE = "content_click";
|
|
6
|
+
/**
|
|
7
|
+
* Default debounce time in milliseconds for clicks
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_CLICK_THROTTLE_MS = 300;
|
|
10
|
+
/**
|
|
11
|
+
* CSS selector for clickable elements to track
|
|
12
|
+
* Only clicks on <a> and <button> elements are tracked
|
|
13
|
+
*/
|
|
14
|
+
export declare const CLICKABLE_ELEMENTS_SELECTOR = "a, button";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PipelineConfig } from '../pipeline/models';
|
|
2
|
+
import type { AnalyticsInstance } from 'analytics';
|
|
3
|
+
/**
|
|
4
|
+
* Clicks plugin of the events pipeline.
|
|
5
|
+
* Handles automatic tracking of clicks on content elements.
|
|
6
|
+
*
|
|
7
|
+
* This plugin initializes the click tracker which:
|
|
8
|
+
* - Listens for clicks once, on the document, in the capture phase
|
|
9
|
+
* - Reports clicks on <a> or <button> elements inside a contentlet, the innermost one
|
|
10
|
+
* - Extracts contentlet data, its position when clicked, and element metadata
|
|
11
|
+
* - Throttles clicks per contentlet to prevent duplicates
|
|
12
|
+
* - Fires 'content_click' events via subscription callback
|
|
13
|
+
*
|
|
14
|
+
* Note: This plugin is only registered if config.clicks is enabled.
|
|
15
|
+
* See getEnhancedTrackingPlugins() for conditional loading logic.
|
|
16
|
+
*
|
|
17
|
+
* @param {PipelineConfig} config - Configuration with clicks settings
|
|
18
|
+
* @returns {Object} Plugin object with lifecycle methods
|
|
19
|
+
*/
|
|
20
|
+
export declare const clicksPlugin: (config: PipelineConfig) => {
|
|
21
|
+
name: string;
|
|
22
|
+
/**
|
|
23
|
+
* Initialize click tracking
|
|
24
|
+
* Called when Analytics.js initializes the plugin with instance context
|
|
25
|
+
* @param instance - Analytics.js instance with track method
|
|
26
|
+
*/
|
|
27
|
+
initialize: ({ instance }: {
|
|
28
|
+
instance: AnalyticsInstance;
|
|
29
|
+
}) => Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Setup cleanup handlers when plugin is loaded
|
|
32
|
+
* Called after Analytics.js completes plugin loading
|
|
33
|
+
*/
|
|
34
|
+
loaded: () => boolean;
|
|
35
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { PipelineConfig, DotCMSContentClickPayload } from '../pipeline/models';
|
|
2
|
+
/** Callback function for click events */
|
|
3
|
+
export type ClickCallback = (eventName: string, payload: DotCMSContentClickPayload) => void;
|
|
4
|
+
/** Subscription object with unsubscribe method */
|
|
5
|
+
export interface ClickSubscription {
|
|
6
|
+
unsubscribe: () => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Tracks clicks on the links and buttons inside contentlets, through one click listener on the
|
|
10
|
+
* document, in the capture phase. A click finds its contentlet with `closest`, so:
|
|
11
|
+
* - nothing scans or observes the page, and a contentlet added at any time counts;
|
|
12
|
+
* - an app's `stopPropagation()` cannot hide a click from the tracker;
|
|
13
|
+
* - a click inside nested contentlets counts once, for the innermost;
|
|
14
|
+
* - clicks on the same contentlet within 300 ms count once.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const tracker = new DotCMSClickTracker(config);
|
|
19
|
+
* const subscription = tracker.onClick((eventName, payload) => {
|
|
20
|
+
* console.log('Click detected:', payload);
|
|
21
|
+
* });
|
|
22
|
+
* tracker.initialize();
|
|
23
|
+
* // Later: subscription.unsubscribe();
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare class DotCMSClickTracker {
|
|
27
|
+
#private;
|
|
28
|
+
constructor(config: PipelineConfig);
|
|
29
|
+
/**
|
|
30
|
+
* Subscribe to click events
|
|
31
|
+
* @param callback - Function called when click is detected
|
|
32
|
+
* @returns Subscription object with unsubscribe method
|
|
33
|
+
*/
|
|
34
|
+
onClick(callback: ClickCallback): ClickSubscription;
|
|
35
|
+
/** Starts listening for clicks; without a document, as on the server, it does nothing */
|
|
36
|
+
initialize(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Stops listening for clicks. Should be called when the plugin is disabled or the page is
|
|
39
|
+
* discarded.
|
|
40
|
+
*/
|
|
41
|
+
cleanup(): void;
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DotCMSContentClickPayload } from '../pipeline/models';
|
|
2
|
+
import type { createPluginLogger } from '../pipeline/utils';
|
|
3
|
+
/**
|
|
4
|
+
* Handles click events on elements within a contentlet.
|
|
5
|
+
* The tracker passes the contentlet the click landed in: the innermost one around the clicked
|
|
6
|
+
* link or button.
|
|
7
|
+
*
|
|
8
|
+
* @param event - The mouse event
|
|
9
|
+
* @param contentletElement - The contentlet container element
|
|
10
|
+
* @param trackCallback - Callback to execute if the click is valid
|
|
11
|
+
* @param logger - Logger instance for debug messages
|
|
12
|
+
*/
|
|
13
|
+
export declare const handleContentletClick: (event: MouseEvent, contentletElement: HTMLElement, trackCallback: (eventName: string, payload: DotCMSContentClickPayload) => void, logger: ReturnType<typeof createPluginLogger>) => void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default debounce time in milliseconds for MutationObserver
|
|
3
|
+
*/
|
|
4
|
+
export declare const CONTENTLET_OBSERVER_DEBOUNCE_MS = 250;
|
|
5
|
+
/**
|
|
6
|
+
* CSS class selector for contentlet elements
|
|
7
|
+
*
|
|
8
|
+
* @important This constant is intentionally duplicated in @dotcms/react SDK
|
|
9
|
+
* (core-web/libs/sdk/react/src/lib/next/components/Contentlet/Contentlet.tsx), whose renderers
|
|
10
|
+
* print the class, and dotCMS prints it on traditional pages too. Both constants MUST have the
|
|
11
|
+
* same value ('dotcms-contentlet') for content tracking to find React-rendered contentlets.
|
|
12
|
+
*
|
|
13
|
+
* This duplication is intentional to keep the SDKs independent:
|
|
14
|
+
* - @dotcms/events works without React
|
|
15
|
+
* - @dotcms/react works without events
|
|
16
|
+
* - When both are used together, they must share the same class name
|
|
17
|
+
*
|
|
18
|
+
* If you need to change this value, you MUST update it in both locations:
|
|
19
|
+
* 1. This file (@dotcms/events)
|
|
20
|
+
* 2. Contentlet.tsx in @dotcms/react
|
|
21
|
+
*
|
|
22
|
+
* @see core-web/libs/sdk/react/src/lib/next/components/Contentlet/Contentlet.tsx
|
|
23
|
+
*/
|
|
24
|
+
export declare const CONTENTLET_CLASS = "dotcms-contentlet";
|
|
25
|
+
/**
|
|
26
|
+
* Attribute that carries a contentlet's identifier (`dataset['dotIdentifier']`). The page
|
|
27
|
+
* renderers print it only once analytics is active, after hydration, so it can land after
|
|
28
|
+
* the contentlet itself.
|
|
29
|
+
*/
|
|
30
|
+
export declare const CONTENTLET_IDENTIFIER_ATTRIBUTE = "data-dot-identifier";
|
|
31
|
+
/**
|
|
32
|
+
* Window event that asks the content trackers to scan for contentlets again. Sent when
|
|
33
|
+
* contentlets that were hidden become visible, which changes no DOM node the trackers watch
|
|
34
|
+
* (an experiment's rows are revealed through a style rule).
|
|
35
|
+
*/
|
|
36
|
+
export declare const CONTENTLET_RESCAN_EVENT = "dotcms:events:rescan";
|