@dotcms/events 0.0.0-stage → 26.10.5-1-next.2827

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.
Files changed (57) hide show
  1. package/README.md +516 -2
  2. package/decision.cjs.js +108 -0
  3. package/decision.esm.js +92 -0
  4. package/index.cjs.default.js +1 -0
  5. package/index.cjs.js +3674 -0
  6. package/index.cjs.mjs +2 -0
  7. package/index.d.ts +1 -0
  8. package/index.esm.js +3672 -0
  9. package/markup.cjs.default.js +1 -0
  10. package/markup.cjs.js +161 -0
  11. package/markup.cjs.mjs +2 -0
  12. package/markup.d.ts +1 -0
  13. package/markup.esm.js +159 -0
  14. package/package.json +77 -4
  15. package/react.cjs.default.js +1 -0
  16. package/react.cjs.js +39 -0
  17. package/react.cjs.mjs +2 -0
  18. package/react.d.ts +1 -0
  19. package/react.esm.js +37 -0
  20. package/src/index.d.ts +8 -0
  21. package/src/lib/clicks/constants.d.ts +14 -0
  22. package/src/lib/clicks/plugin.d.ts +35 -0
  23. package/src/lib/clicks/tracker.d.ts +42 -0
  24. package/src/lib/clicks/utils.d.ts +13 -0
  25. package/src/lib/contentlets/constants.d.ts +36 -0
  26. package/src/lib/contentlets/utils.d.ts +64 -0
  27. package/src/lib/contentlets/viewport.d.ts +23 -0
  28. package/src/lib/events.d.ts +17 -0
  29. package/src/lib/experiments/api.d.ts +9 -0
  30. package/src/lib/experiments/boot.d.ts +53 -0
  31. package/src/lib/experiments/constants.d.ts +46 -0
  32. package/src/lib/experiments/decision.d.ts +53 -0
  33. package/src/lib/experiments/dom.d.ts +62 -0
  34. package/src/lib/experiments/engine.d.ts +66 -0
  35. package/src/lib/experiments/markup.d.ts +17 -0
  36. package/src/lib/experiments/models.d.ts +62 -0
  37. package/src/lib/experiments/plugin.d.ts +27 -0
  38. package/src/lib/experiments/store.d.ts +54 -0
  39. package/src/lib/impressions/constants.d.ts +28 -0
  40. package/src/lib/impressions/plugin.d.ts +38 -0
  41. package/src/lib/impressions/tracker.d.ts +67 -0
  42. package/src/lib/impressions/utils.d.ts +10 -0
  43. package/src/lib/models.d.ts +130 -0
  44. package/src/lib/pipeline/constants.d.ts +88 -0
  45. package/src/lib/pipeline/enricher/plugin.d.ts +32 -0
  46. package/src/lib/pipeline/identity/activity.d.ts +19 -0
  47. package/src/lib/pipeline/identity/plugin.d.ts +60 -0
  48. package/src/lib/pipeline/logger.d.ts +74 -0
  49. package/src/lib/pipeline/models.d.ts +588 -0
  50. package/src/lib/pipeline/navigation.d.ts +21 -0
  51. package/src/lib/pipeline/sender/http.d.ts +9 -0
  52. package/src/lib/pipeline/sender/plugin.d.ts +47 -0
  53. package/src/lib/pipeline/sender/queue.d.ts +33 -0
  54. package/src/lib/pipeline/utils.d.ts +119 -0
  55. package/src/lib/react/DotCMSExperiment.d.ts +36 -0
  56. package/src/markup.d.ts +7 -0
  57. 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
@@ -0,0 +1,2 @@
1
+ export * from './markup.cjs.js';
2
+ export { _default as default } from './markup.cjs.default.js';
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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "26.10.5-1-next.2827",
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-next.2827",
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-next.2827"
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
@@ -0,0 +1,2 @@
1
+ export * from './react.cjs.js';
2
+ export { _default as default } from './react.cjs.default.js';
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";