@mfeflow/analytics 0.1.0 → 0.3.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 +11 -1
- package/dist/clarity.d.ts +18 -0
- package/dist/clarity.js +66 -0
- package/dist/combine.d.ts +8 -0
- package/dist/combine.js +48 -0
- package/dist/core.d.ts +10 -0
- package/dist/core.js +35 -2
- package/dist/providers.d.ts +13 -0
- package/dist/providers.js +11 -0
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -34,11 +34,21 @@ export const tracker = createTracker('cart', { events: ['add_to_cart'] });
|
|
|
34
34
|
tracker.track('add_to_cart', { sku: 'A1' });
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
To run a second provider (Microsoft Clarity, which loads only after analytics consent), join it with `addProvider`; both then get every call:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { addProvider } from '@mfeflow/analytics/providers';
|
|
41
|
+
import { createClarityProvider } from '@mfeflow/analytics/clarity';
|
|
42
|
+
|
|
43
|
+
addProvider(createGtmProvider({ containerId: 'GTM-ABC123' }));
|
|
44
|
+
addProvider(createClarityProvider({ projectId: 'k3x9m2abcd' }));
|
|
45
|
+
```
|
|
46
|
+
|
|
37
47
|
The GTM provider pushes the four consent signals as `denied` to `dataLayer` before it requests `gtm.js`, and drops parameters that look like personal data.
|
|
38
48
|
|
|
39
49
|
## Module Federation sharing
|
|
40
50
|
|
|
41
|
-
Bucket: **singleton**. Share `@mfeflow/analytics` and `@mfeflow/analytics/react` (`@mfeflow/rsbuild` does). State also lives on `globalThis`, so a second copy still talks to the one provider. `@mfeflow/analytics/gtm`
|
|
51
|
+
Bucket: **singleton**. Share `@mfeflow/analytics` and `@mfeflow/analytics/react` (`@mfeflow/rsbuild` does). State also lives on `globalThis`, so a second copy still talks to the one provider. `@mfeflow/analytics/gtm`, `/clarity` and `/providers` are imported by the shell only; `addProvider` lives in its own entry point so the shared module that remotes load stays small.
|
|
42
52
|
|
|
43
53
|
## Links
|
|
44
54
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AnalyticsProvider } from './types.js';
|
|
2
|
+
export interface ClarityOptions {
|
|
3
|
+
/** The Clarity project id, like `k3x9m2abcd`. From the runtime config, never baked into the bundle. */
|
|
4
|
+
projectId: string;
|
|
5
|
+
/** Default Clarity's own `https://www.clarity.ms/tag/`. Tests point it elsewhere. */
|
|
6
|
+
scriptUrl?: string | undefined;
|
|
7
|
+
/** Send the id given to `identify` as Clarity's custom user id. Off by default; traits are never sent. */
|
|
8
|
+
sendUserId?: boolean | undefined;
|
|
9
|
+
}
|
|
10
|
+
export declare const CLARITY_SCRIPT_URL = "https://www.clarity.ms/tag/";
|
|
11
|
+
export declare const CLARITY_SCRIPT_ATTRIBUTE = "data-mfeflow-clarity";
|
|
12
|
+
/**
|
|
13
|
+
* Microsoft Clarity behind the analytics interface, gated on consent. Nothing is requested, and no cookie is
|
|
14
|
+
* set, until `analytics_storage` is `granted`: the script is added on that choice (now, or on a later visit
|
|
15
|
+
* when the stored consent arrives in `init`). A later `denied` tells Clarity to stop and clear its cookies.
|
|
16
|
+
* Until then every call is dropped, never kept for later.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createClarityProvider(options: ClarityOptions): AnalyticsProvider;
|
package/dist/clarity.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const PROJECT_ID = /^[a-z0-9]{8,12}$/;
|
|
2
|
+
const CLARITY_SCRIPT_URL = 'https://www.clarity.ms/tag/';
|
|
3
|
+
const CLARITY_SCRIPT_ATTRIBUTE = 'data-mfeflow-clarity';
|
|
4
|
+
const isClarity = (value)=>'function' == typeof value;
|
|
5
|
+
function clarityFunction() {
|
|
6
|
+
const existing = Reflect.get(globalThis, 'clarity');
|
|
7
|
+
if (isClarity(existing)) return existing;
|
|
8
|
+
const queue = function() {
|
|
9
|
+
(queue.q ??= []).push(arguments);
|
|
10
|
+
};
|
|
11
|
+
Reflect.set(globalThis, 'clarity', queue);
|
|
12
|
+
return queue;
|
|
13
|
+
}
|
|
14
|
+
function createClarityProvider(options) {
|
|
15
|
+
const { projectId } = options;
|
|
16
|
+
if (!PROJECT_ID.test(projectId)) throw new Error(`"${projectId}" is not a Clarity project id (8 to 12 lowercase letters and digits)`);
|
|
17
|
+
let consent = {};
|
|
18
|
+
let started = false;
|
|
19
|
+
const sendConsent = ()=>{
|
|
20
|
+
clarityFunction()('consentv2', {
|
|
21
|
+
ad_Storage: consent.ad_storage ?? 'denied',
|
|
22
|
+
analytics_Storage: consent.analytics_storage ?? 'denied'
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
const start = ()=>{
|
|
26
|
+
if (started) return;
|
|
27
|
+
started = true;
|
|
28
|
+
const selector = `script[${CLARITY_SCRIPT_ATTRIBUTE}="${projectId}"]`;
|
|
29
|
+
clarityFunction();
|
|
30
|
+
sendConsent();
|
|
31
|
+
if (document.querySelector(selector)) return;
|
|
32
|
+
const script = document.createElement("script");
|
|
33
|
+
script.async = true;
|
|
34
|
+
script.setAttribute(CLARITY_SCRIPT_ATTRIBUTE, projectId);
|
|
35
|
+
const base = options.scriptUrl ?? CLARITY_SCRIPT_URL;
|
|
36
|
+
script.src = new URL(projectId, base.endsWith('/') ? base : `${base}/`).href;
|
|
37
|
+
document.head.append(script);
|
|
38
|
+
};
|
|
39
|
+
const apply = (update)=>{
|
|
40
|
+
consent = {
|
|
41
|
+
...consent,
|
|
42
|
+
...update
|
|
43
|
+
};
|
|
44
|
+
if (started) sendConsent();
|
|
45
|
+
else if ('granted' === consent.analytics_storage) start();
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
name: 'clarity',
|
|
49
|
+
init (stored) {
|
|
50
|
+
apply(stored);
|
|
51
|
+
},
|
|
52
|
+
track (event) {
|
|
53
|
+
if (started && 'granted' === consent.analytics_storage) clarityFunction()('event', event);
|
|
54
|
+
},
|
|
55
|
+
page (path) {
|
|
56
|
+
if (started && 'granted' === consent.analytics_storage) clarityFunction()('set', 'page_path', path);
|
|
57
|
+
},
|
|
58
|
+
identify (userId) {
|
|
59
|
+
if (true === options.sendUserId && started && 'granted' === consent.analytics_storage) clarityFunction()('identify', userId);
|
|
60
|
+
},
|
|
61
|
+
setConsent (update) {
|
|
62
|
+
apply(update);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export { CLARITY_SCRIPT_ATTRIBUTE, CLARITY_SCRIPT_URL, createClarityProvider };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AnalyticsProvider } from './types.js';
|
|
2
|
+
/** The providers a combined provider sends to (itself, if it is not a combination). */
|
|
3
|
+
export declare function partsOf(provider: AnalyticsProvider): readonly AnalyticsProvider[];
|
|
4
|
+
/**
|
|
5
|
+
* One provider that forwards every call to all of `providers`, in order. A provider that throws is
|
|
6
|
+
* reported once and skipped for that call, so a failing tag cannot stop the others.
|
|
7
|
+
*/
|
|
8
|
+
export declare function combineProviders(...providers: AnalyticsProvider[]): AnalyticsProvider;
|
package/dist/combine.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { warnOnce } from "./validate.js";
|
|
2
|
+
const members = new WeakMap();
|
|
3
|
+
function partsOf(provider) {
|
|
4
|
+
return members.get(provider) ?? [
|
|
5
|
+
provider
|
|
6
|
+
];
|
|
7
|
+
}
|
|
8
|
+
function combineProviders(...providers) {
|
|
9
|
+
const parts = providers.flatMap(partsOf);
|
|
10
|
+
const each = (what, call)=>{
|
|
11
|
+
for (const part of parts)try {
|
|
12
|
+
call(part);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
warnOnce(`${part.name}: ${what} failed (${error instanceof Error ? error.message : 'unknown error'})`);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const combined = {
|
|
18
|
+
name: parts.map((p)=>p.name).join('+'),
|
|
19
|
+
init: (consent)=>{
|
|
20
|
+
each('init', (p)=>{
|
|
21
|
+
p.init(consent);
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
track: (event, params)=>{
|
|
25
|
+
each('track', (p)=>{
|
|
26
|
+
p.track(event, params);
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
page: (path, params)=>{
|
|
30
|
+
each('page', (p)=>{
|
|
31
|
+
p.page(path, params);
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
identify: (userId, traits)=>{
|
|
35
|
+
each('identify', (p)=>{
|
|
36
|
+
p.identify(userId, traits);
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
setConsent: (update)=>{
|
|
40
|
+
each('setConsent', (p)=>{
|
|
41
|
+
p.setConsent(update);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
members.set(combined, parts);
|
|
46
|
+
return combined;
|
|
47
|
+
}
|
|
48
|
+
export { combineProviders, partsOf };
|
package/dist/core.d.ts
CHANGED
|
@@ -4,6 +4,16 @@ import type { AnalyticsProvider, ConsentUpdate, EventParams } from './types.js';
|
|
|
4
4
|
* (a second mount, StrictMode, a remote running on its own) do nothing and return false.
|
|
5
5
|
*/
|
|
6
6
|
export declare function initAnalytics(provider: AnalyticsProvider): boolean;
|
|
7
|
+
/** The provider that is running, if any. Used by `addProvider` in `./providers`. */
|
|
8
|
+
export declare function runningProvider(): AnalyticsProvider | undefined;
|
|
9
|
+
/** For `./providers`: from now on, keep the first calls (for ten seconds, at most 100) for providers that join later. */
|
|
10
|
+
export declare function keepCallsForJoiners(): void;
|
|
11
|
+
/**
|
|
12
|
+
* Starts `added` with the stored consent, and only then makes `joined` (a combination that includes it) the
|
|
13
|
+
* running provider, so a provider whose `init` throws leaves the running one as it was and can be added
|
|
14
|
+
* again. Then sends it the calls made since analytics began, if they are still kept.
|
|
15
|
+
*/
|
|
16
|
+
export declare function joinProvider(joined: AnalyticsProvider, added: AnalyticsProvider): void;
|
|
7
17
|
export declare function isAnalyticsReady(): boolean;
|
|
8
18
|
export declare function providerName(): string;
|
|
9
19
|
export declare function track(event: string, params?: EventParams): void;
|
package/dist/core.js
CHANGED
|
@@ -2,6 +2,8 @@ import { CONSENT_STORAGE_KEY, cleanConsent, readStoredConsent, writeStoredConsen
|
|
|
2
2
|
import { noopProvider } from "./noop.js";
|
|
3
3
|
const KEY = Symbol.for('mfeflow.analytics');
|
|
4
4
|
const QUEUE_LIMIT = 100;
|
|
5
|
+
const BOOT_WINDOW_MS = 10000;
|
|
6
|
+
let keepCalls = false;
|
|
5
7
|
function isState(value) {
|
|
6
8
|
return 'object' == typeof value && null !== value && 'queue' in value && 'listeners' in value;
|
|
7
9
|
}
|
|
@@ -13,13 +15,23 @@ function state() {
|
|
|
13
15
|
queue: [],
|
|
14
16
|
consent: readStoredConsent(),
|
|
15
17
|
listeners: new Set(),
|
|
16
|
-
lastPage: void 0
|
|
18
|
+
lastPage: void 0,
|
|
19
|
+
replay: keepCalls ? {
|
|
20
|
+
calls: [],
|
|
21
|
+
since: void 0
|
|
22
|
+
} : void 0
|
|
17
23
|
};
|
|
18
24
|
Reflect.set(globalThis, KEY, fresh);
|
|
19
25
|
return fresh;
|
|
20
26
|
}
|
|
21
27
|
function send(call) {
|
|
22
28
|
const s = state();
|
|
29
|
+
const keep = s.replay;
|
|
30
|
+
if (keep) {
|
|
31
|
+
keep.since ??= Date.now();
|
|
32
|
+
if (Date.now() - keep.since > BOOT_WINDOW_MS) s.replay = void 0;
|
|
33
|
+
else if (keep.calls.length < QUEUE_LIMIT) keep.calls.push(call);
|
|
34
|
+
}
|
|
23
35
|
if (s.provider) call(s.provider);
|
|
24
36
|
else if (s.queue.length < QUEUE_LIMIT) s.queue.push(call);
|
|
25
37
|
}
|
|
@@ -34,6 +46,27 @@ function initAnalytics(provider) {
|
|
|
34
46
|
for (const call of queued)call(provider);
|
|
35
47
|
return true;
|
|
36
48
|
}
|
|
49
|
+
function runningProvider() {
|
|
50
|
+
return state().provider;
|
|
51
|
+
}
|
|
52
|
+
function keepCallsForJoiners() {
|
|
53
|
+
keepCalls = true;
|
|
54
|
+
const s = state();
|
|
55
|
+
s.replay ??= {
|
|
56
|
+
calls: [],
|
|
57
|
+
since: void 0
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function joinProvider(joined, added) {
|
|
61
|
+
const s = state();
|
|
62
|
+
added.init({
|
|
63
|
+
...s.consent
|
|
64
|
+
});
|
|
65
|
+
s.provider = joined;
|
|
66
|
+
for (const call of s.replay?.calls ?? [])try {
|
|
67
|
+
call(added);
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
37
70
|
function isAnalyticsReady() {
|
|
38
71
|
return void 0 !== state().provider;
|
|
39
72
|
}
|
|
@@ -93,4 +126,4 @@ function resetAnalyticsForTests() {
|
|
|
93
126
|
localStorage.removeItem(CONSENT_STORAGE_KEY);
|
|
94
127
|
} catch {}
|
|
95
128
|
}
|
|
96
|
-
export { getConsent, hasConsentChoice, identify, initAnalytics, isAnalyticsReady, page, pageOnce, providerName, resetAnalyticsForTests, subscribeConsent, track, updateConsent };
|
|
129
|
+
export { getConsent, hasConsentChoice, identify, initAnalytics, isAnalyticsReady, joinProvider, keepCallsForJoiners, page, pageOnce, providerName, resetAnalyticsForTests, runningProvider, subscribeConsent, track, updateConsent };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { combineProviders } from './combine.js';
|
|
2
|
+
import type { AnalyticsProvider } from './types.js';
|
|
3
|
+
export { combineProviders };
|
|
4
|
+
/**
|
|
5
|
+
* Starts analytics with a provider, or joins one that is already running: from then on both get every
|
|
6
|
+
* call, and the newcomer first gets the calls made in the first ten seconds (at most 100), so the first page view reaches it
|
|
7
|
+
* whichever provider started first. A provider drops what it must, by its own rule (Clarity drops events from before consent). Returns false if a provider with
|
|
8
|
+
* this name is already there. Use it where several features may start a provider in either order.
|
|
9
|
+
*
|
|
10
|
+
* This lives in its own entry point, for the shell only, so the shared `@mfeflow/analytics` module that
|
|
11
|
+
* every remote loads stays as small as it was.
|
|
12
|
+
*/
|
|
13
|
+
export declare function addProvider(provider: AnalyticsProvider): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { combineProviders, partsOf } from "./combine.js";
|
|
2
|
+
import { initAnalytics, joinProvider, keepCallsForJoiners, runningProvider } from "./core.js";
|
|
3
|
+
keepCallsForJoiners();
|
|
4
|
+
function addProvider(provider) {
|
|
5
|
+
const running = runningProvider();
|
|
6
|
+
if (!running) return initAnalytics(provider);
|
|
7
|
+
if (partsOf(running).some((p)=>p.name === provider.name)) return false;
|
|
8
|
+
joinProvider(combineProviders(running, provider), provider);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
export { addProvider, combineProviders };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mfeflow/analytics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Analytics for micro-frontends behind one provider interface: typed events, a tracker that stamps mfe_name, Consent Mode v2, and a Google Tag Manager provider.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"micro-frontend",
|
|
@@ -32,6 +32,16 @@
|
|
|
32
32
|
"import": "./dist/gtm.js",
|
|
33
33
|
"default": "./dist/gtm.js"
|
|
34
34
|
},
|
|
35
|
+
"./clarity": {
|
|
36
|
+
"types": "./dist/clarity.d.ts",
|
|
37
|
+
"import": "./dist/clarity.js",
|
|
38
|
+
"default": "./dist/clarity.js"
|
|
39
|
+
},
|
|
40
|
+
"./providers": {
|
|
41
|
+
"types": "./dist/providers.d.ts",
|
|
42
|
+
"import": "./dist/providers.js",
|
|
43
|
+
"default": "./dist/providers.js"
|
|
44
|
+
},
|
|
35
45
|
"./react": {
|
|
36
46
|
"types": "./dist/react.d.ts",
|
|
37
47
|
"import": "./dist/react.js",
|