@autobusal/providers 1.44.5 → 1.45.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/Setup/analytics.ts +85 -67
- package/package.json +1 -1
- package/types/agents.ts +3 -0
- package/types/users.ts +3 -0
package/Setup/analytics.ts
CHANGED
|
@@ -27,47 +27,84 @@ declare global {
|
|
|
27
27
|
|
|
28
28
|
const CONTAINER_ID = 'gtm-container';
|
|
29
29
|
|
|
30
|
+
declare global {
|
|
31
|
+
interface Window {
|
|
32
|
+
gtag?: (...args: any[]) => void
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const CONSENT_TYPES = ['ad_storage', 'ad_user_data', 'ad_personalization', 'analytics_storage'];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Google Consent Mode v2: the signal Tag Manager and GA4 read before any tag
|
|
40
|
+
* fires. Pushed onto the dataLayer through the gtag() shim, the way Google
|
|
41
|
+
* documents it, so it is in place before gtm.js is even requested.
|
|
42
|
+
*/
|
|
43
|
+
const consentSignal = (command: 'default' | 'update', granted: boolean): void => {
|
|
44
|
+
window.dataLayer = window.dataLayer || [];
|
|
45
|
+
|
|
46
|
+
window.gtag = window.gtag || function () {
|
|
47
|
+
// eslint-disable-next-line prefer-rest-params
|
|
48
|
+
window.dataLayer!.push(arguments);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const state = Object.fromEntries(CONSENT_TYPES.map(type => [ type, granted ? 'granted' : 'denied' ]));
|
|
52
|
+
|
|
53
|
+
window.gtag('consent', command, command === 'default' ? { ...state, wait_for_update: 500 } : state);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Claude - 2026-09-25 (Ferjolt: consent, not a notice).
|
|
58
|
+
*
|
|
59
|
+
* NOTHING NON-ESSENTIAL LOADS UNTIL THE VISITOR ACCEPTS. Tag Manager (and
|
|
60
|
+
* GA4 and Microsoft Clarity, which it carries) used to load on every page
|
|
61
|
+
* while the cookie card said "by continuing you accept". Now the loader
|
|
62
|
+
* reads the recorded choice (@autobusal/common CookieNotification/consent):
|
|
63
|
+
* accepted loads the container as before; nothing recorded, or rejected,
|
|
64
|
+
* loads nothing and waits for the CONSENT_EVENT the card fires - so a
|
|
65
|
+
* visitor who accepts mid-visit gets the container then, together with
|
|
66
|
+
* every dataLayer event pushed in the meantime (they queue in the array).
|
|
67
|
+
*
|
|
68
|
+
* Consent Mode is signalled either way, so if the container is ever loaded
|
|
69
|
+
* with consent denied (a tag added later, say) Google's tags behave.
|
|
70
|
+
*/
|
|
71
|
+
const CONSENT_COOKIE = 'OBTCookieConsent';
|
|
72
|
+
const CONSENT_EVENT = 'obt:cookie-consent';
|
|
73
|
+
|
|
74
|
+
const storedConsent = (): 'accepted' | 'rejected' | null => {
|
|
75
|
+
const match = document.cookie.match(new RegExp('(?:^|; )' + CONSENT_COOKIE + '=(accepted|rejected)'));
|
|
76
|
+
|
|
77
|
+
return match ? (match[1] as 'accepted' | 'rejected') : null;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Setup renders more than once; the consent signal and the listener must
|
|
81
|
+
// exist exactly once per page, whatever the container element says
|
|
82
|
+
let armed = false;
|
|
83
|
+
|
|
30
84
|
const analytics = (container: string | null | undefined): void => {
|
|
31
|
-
if (!container) {
|
|
85
|
+
if (!container || armed) {
|
|
32
86
|
return;
|
|
33
87
|
}
|
|
34
88
|
|
|
35
|
-
// Guard against a second insert. Two containers on one page double-count
|
|
36
|
-
// every tag they fire, and Setup can re-run (a settings refetch, a
|
|
37
|
-
// remount) without the page ever reloading.
|
|
38
89
|
if (document.getElementById(CONTAINER_ID)) {
|
|
39
90
|
return;
|
|
40
91
|
}
|
|
41
92
|
|
|
93
|
+
armed = true;
|
|
94
|
+
|
|
42
95
|
window.dataLayer = window.dataLayer || [];
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
*
|
|
49
|
-
* Edited: Claude - Date: 2026-09-16
|
|
50
|
-
*
|
|
51
|
-
* Measured with Lighthouse against production on a throttled phone: the
|
|
52
|
-
* container and the GA tag it pulls are 287 KB of script - more than this
|
|
53
|
-
* app's own entry chunk - fetched in the same window as the bundle, the
|
|
54
|
-
* translations and the images, on a connection modelled at 1.6 Mbps. The
|
|
55
|
-
* page is no more usable for having them early, and every byte they take
|
|
56
|
-
* is one the content is waiting for.
|
|
57
|
-
*
|
|
58
|
-
* The dataLayer is filled FIRST, above, and GTM reads whatever is already
|
|
59
|
-
* in it when it boots - so a page view or a commerce event announced in
|
|
60
|
-
* the meantime still arrives. That is the only thing that could have made
|
|
61
|
-
* deferring this wrong.
|
|
62
|
-
*
|
|
63
|
-
* Safari has no requestIdleCallback, so setTimeout is the fallback rather
|
|
64
|
-
* than a polyfill: "soon, after the load" is the whole requirement.
|
|
65
|
-
*/
|
|
96
|
+
|
|
97
|
+
const accepted = storedConsent() === 'accepted';
|
|
98
|
+
|
|
99
|
+
consentSignal('default', accepted);
|
|
100
|
+
|
|
66
101
|
const load = (): void => {
|
|
67
102
|
if (document.getElementById(CONTAINER_ID)) {
|
|
68
103
|
return;
|
|
69
104
|
}
|
|
70
105
|
|
|
106
|
+
window.dataLayer!.push({ 'gtm.start': Date.now(), event: 'gtm.js' });
|
|
107
|
+
|
|
71
108
|
const script = document.createElement('script');
|
|
72
109
|
|
|
73
110
|
script.id = CONTAINER_ID;
|
|
@@ -77,32 +114,6 @@ const analytics = (container: string | null | undefined): void => {
|
|
|
77
114
|
document.head.appendChild(script);
|
|
78
115
|
};
|
|
79
116
|
|
|
80
|
-
/*
|
|
81
|
-
* AFTER THE PAGE HAS LOADED, then in the first quiet moment.
|
|
82
|
-
*
|
|
83
|
-
* Edited: Claude - Date: 2026-09-16
|
|
84
|
-
*
|
|
85
|
-
* The idle wait above was not enough on its own, because "idle" arrives
|
|
86
|
-
* early on a page that is still downloading: requestIdleCallback fires
|
|
87
|
-
* whenever the main thread has a gap, and a phone waiting on the network
|
|
88
|
-
* has plenty of gaps. Lighthouse on a throttled phone still found Tag
|
|
89
|
-
* Manager and the Google Analytics tag it pulls in - 287 KB between them,
|
|
90
|
-
* 131 KB of it never executed - arriving inside the page's own loading
|
|
91
|
-
* window, competing for the same connection as the bundle and the images.
|
|
92
|
-
*
|
|
93
|
-
* Waiting for `load` first puts them after everything the page itself
|
|
94
|
-
* asked for. The idle wait then keeps them off the main thread while the
|
|
95
|
-
* page settles, with a ceiling so they are never deferred indefinitely.
|
|
96
|
-
*
|
|
97
|
-
* What this costs is measurement, not function, and it is worth naming: a
|
|
98
|
-
* visitor who leaves within a couple of seconds of the page finishing
|
|
99
|
-
* loading may not be counted. Everything else is kept. The dataLayer is
|
|
100
|
-
* created and filled BEFORE any of this, so page views and commerce
|
|
101
|
-
* events announced while the container is on its way are queued and
|
|
102
|
-
* processed when it boots, not dropped.
|
|
103
|
-
*
|
|
104
|
-
* Safari has no requestIdleCallback, so a short timeout stands in for it.
|
|
105
|
-
*/
|
|
106
117
|
const whenIdle = (): void => {
|
|
107
118
|
if (typeof window.requestIdleCallback === 'function') {
|
|
108
119
|
window.requestIdleCallback(load, { timeout: 3000 });
|
|
@@ -111,23 +122,30 @@ const analytics = (container: string | null | undefined): void => {
|
|
|
111
122
|
}
|
|
112
123
|
};
|
|
113
124
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
125
|
+
const start = (): void => {
|
|
126
|
+
if (document.readyState === 'complete') {
|
|
127
|
+
whenIdle();
|
|
128
|
+
} else {
|
|
129
|
+
window.addEventListener('load', whenIdle, { once: true });
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (accepted) {
|
|
134
|
+
start();
|
|
135
|
+
|
|
136
|
+
return;
|
|
118
137
|
}
|
|
138
|
+
|
|
139
|
+
// no choice yet, or rejected: wait for the card
|
|
140
|
+
window.addEventListener(CONSENT_EVENT, (event: Event) => {
|
|
141
|
+
if ((event as CustomEvent<string>).detail === 'accepted') {
|
|
142
|
+
consentSignal('update', true);
|
|
143
|
+
|
|
144
|
+
start();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
119
147
|
};
|
|
120
148
|
|
|
121
|
-
/**
|
|
122
|
-
* Announce a page view.
|
|
123
|
-
*
|
|
124
|
-
* A single-page app never reloads, so GTM's own page-load trigger fires
|
|
125
|
-
* exactly once per session - every client-side route change after that is
|
|
126
|
-
* invisible unless it is announced. The container is expected to listen for
|
|
127
|
-
* this `page_view` event and fire GA4 (or anything else) from it.
|
|
128
|
-
*
|
|
129
|
-
* Safe to call before GTM has finished loading: the push simply queues.
|
|
130
|
-
*/
|
|
131
149
|
export const pageview = (path: string, title: string): void => {
|
|
132
150
|
window.dataLayer = window.dataLayer || [];
|
|
133
151
|
|
package/package.json
CHANGED
package/types/agents.ts
CHANGED
|
@@ -20,6 +20,9 @@ export interface AgentData {
|
|
|
20
20
|
// Claude - 2026-08-22: the floor of the funds balance for website sales
|
|
21
21
|
// (0 = prepaid, positive = postpaid down to -that-much)
|
|
22
22
|
credit_limit?: number
|
|
23
|
+
// Claude - 2026-09-24: the agency has partner API access (the one gate
|
|
24
|
+
// Http\Middleware\ApiConsumer accepts); set by "Enable partner API"
|
|
25
|
+
is_api_consumer?: boolean
|
|
23
26
|
mobile: string
|
|
24
27
|
mobile2: string
|
|
25
28
|
phone: string
|
package/types/users.ts
CHANGED
|
@@ -60,6 +60,9 @@ export interface ComissionData {
|
|
|
60
60
|
|
|
61
61
|
export interface ApiData {
|
|
62
62
|
api: number
|
|
63
|
+
// Claude - 2026-09-24: whether a partner API key exists - the key itself is
|
|
64
|
+
// never sent (Agents\AdminController::get, ApiConsumers\AdminController::get)
|
|
65
|
+
has_token?: boolean
|
|
63
66
|
bkt: number
|
|
64
67
|
devpos: number
|
|
65
68
|
stripe: number
|