@oneuptime/common 12.0.19 → 12.0.20

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 (66) hide show
  1. package/Server/API/EnterpriseLicenseAPI.ts +94 -19
  2. package/Server/EnvironmentConfig.ts +17 -0
  3. package/Server/Services/ProjectService.ts +104 -0
  4. package/Server/Utils/Response.ts +2 -2
  5. package/Server/Utils/StartServer.ts +2 -2
  6. package/Server/Views/Partials/AnalyticsConsent.ejs +533 -0
  7. package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
  8. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +302 -0
  9. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
  10. package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
  11. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
  12. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
  13. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
  14. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
  15. package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
  16. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
  17. package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
  18. package/Tests/UI/Components/Charts/TooltipEntries.test.ts +155 -0
  19. package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
  20. package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
  21. package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
  22. package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
  23. package/Types/Analytics/RevenueEvent.ts +1 -0
  24. package/Types/Marketing/MarketingEvent.ts +49 -6
  25. package/UI/Components/Charts/Area/AreaChart.tsx +8 -1
  26. package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
  27. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +19 -4
  28. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +20 -2
  29. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +19 -4
  30. package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +105 -0
  31. package/UI/Components/Charts/Line/LineChart.tsx +8 -1
  32. package/UI/Components/Detail/Detail.tsx +18 -1
  33. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
  34. package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
  35. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  36. package/build/dist/Server/EnvironmentConfig.js +15 -0
  37. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  38. package/build/dist/Server/Services/ProjectService.js +88 -0
  39. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  40. package/build/dist/Server/Utils/Response.js +2 -2
  41. package/build/dist/Server/Utils/Response.js.map +1 -1
  42. package/build/dist/Server/Utils/StartServer.js +2 -2
  43. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  44. package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
  45. package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
  46. package/build/dist/Types/Marketing/MarketingEvent.js +40 -1
  47. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
  48. package/build/dist/UI/Components/Charts/Area/AreaChart.js +9 -1
  49. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  50. package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
  51. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  52. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +22 -13
  53. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  54. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +27 -15
  55. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  56. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +27 -18
  57. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  58. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +71 -0
  59. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
  60. package/build/dist/UI/Components/Charts/Line/LineChart.js +9 -1
  61. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  62. package/build/dist/UI/Components/Detail/Detail.js +17 -1
  63. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  64. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
  65. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
  66. package/package.json +1 -1
@@ -0,0 +1,533 @@
1
+ <%#
2
+ Consent Mode v2 and attribution capture, shared by every OneUptime-owned
3
+ origin that loads the Google Tag Manager container.
4
+
5
+ This lived only in Home/Views/head-basic.ejs, which meant the marketing site
6
+ denied storage by default and asked the visitor, while /accounts and
7
+ /dashboard loaded the same container with no consent signal at all and
8
+ captured nothing. Two consequences, both bad: somebody who pressed "Reject
9
+ all" on the marketing site was tracked anyway the moment they reached the
10
+ signup page — which is exactly where the conversion fires — and an ad
11
+ pointed straight at /accounts/register?gclid=... stored no click id, so
12
+ that signup was permanently unattributable.
13
+
14
+ It is one file rather than a copy per origin because the storage contract
15
+ has to stay byte-identical: the same 'cookiesAccepted' values, the same
16
+ 'clickIds' and 'firstTouch' JSON shapes. Three copies is how those stop
17
+ agreeing.
18
+
19
+ Include this BEFORE the Google Tag Manager snippet. Consent Mode defaults
20
+ are only meaningful if they are on the dataLayer before the container reads
21
+ it.
22
+
23
+ Home resolves this as ../../Common/Server/Views/Partials/AnalyticsConsent
24
+ and the App FeatureSets as ../../../../Common/Server/Views/Partials/...;
25
+ /usr/src/Common is present in both images.
26
+
27
+ Deliberately NOT here: the PostHog loader (its project key is Home's) and
28
+ the home/-prefixed page view and CTA listeners. Those are Home's alone and
29
+ stay in head-basic.ejs. Reporting on a captured touch is handed back to
30
+ the page through window.oneUptimeOnAttributionCaptured for the same
31
+ reason. The Cal booking helper does live here: it is a pure reader over
32
+ the shared attribution store, and splitting it from the store it reads
33
+ would be worse than sharing an unused function definition.
34
+ -%>
35
+ <!--
36
+ Consent.
37
+
38
+ This block runs BEFORE Google Tag Manager and before PostHog, because both
39
+ of the things it does are only meaningful ahead of them: Consent Mode's
40
+ defaults have to be on the dataLayer before the container reads it, and
41
+ PostHog must not be loaded at all until there is consent to load it.
42
+
43
+ Three states, not two. `granted` and `denied` are the visitor's explicit
44
+ choices; `unset` is the state before they have answered, and it is treated
45
+ as denied for storage purposes rather than as permission-by-silence.
46
+
47
+ What that costs, stated plainly: a visitor who never touches the banner is
48
+ not measured, and their attribution is not stored. What stops that from
49
+ losing an ad click outright is the pending-attribution buffer further
50
+ down — attribution seen on this page is held in memory and written the
51
+ moment consent is granted, so accepting the banner on the landing page
52
+ keeps everything the click carried.
53
+ -->
54
+ <% if(typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false){ %>
55
+ <!--
56
+ The dataLayer and the gtag shim, defined before both the consent signals
57
+ below and the container itself.
58
+
59
+ Deliberately inside the enableGoogleTagManager gate. A self-hosted install
60
+ loads no Google tag at all, and every OneUptime event guards on
61
+ `typeof dataLayer !== 'undefined'` — so creating the array there would turn
62
+ those no-ops into an array that grows for the life of the page and that
63
+ nothing will ever read.
64
+ -->
65
+ <script>
66
+ window.dataLayer = window.dataLayer || [];
67
+ function gtag() { dataLayer.push(arguments); }
68
+ </script>
69
+ <% } %>
70
+
71
+ <script>
72
+ window.oneUptimeConsent = (function () {
73
+ var STORAGE_KEY = 'cookiesAccepted';
74
+ var listeners = [];
75
+
76
+ function readRaw() {
77
+ try {
78
+ return window.localStorage.getItem(STORAGE_KEY);
79
+ } catch (e) {
80
+ /*
81
+ * Private mode, disabled storage, or a cross-origin sandbox.
82
+ * Unreadable storage is not consent.
83
+ */
84
+ return null;
85
+ }
86
+ }
87
+
88
+ function state() {
89
+ var raw = readRaw();
90
+ if (raw === 'true') { return 'granted'; }
91
+ if (raw === 'false') { return 'denied'; }
92
+ return 'unset';
93
+ }
94
+
95
+ function isGranted() {
96
+ return state() === 'granted';
97
+ }
98
+
99
+ /*
100
+ * Consent Mode v2. `security_storage` is granted because it covers
101
+ * things like fraud prevention that are not analytics; everything that
102
+ * feeds advertising or measurement starts denied.
103
+ *
104
+ * wait_for_update gives the container half a second to see an update
105
+ * before it decides how to behave, which is what makes a visitor who
106
+ * accepts immediately still count.
107
+ */
108
+ function pushConsentSignal(mode, granted) {
109
+ /*
110
+ * No tag, nothing to tell. On a self-hosted install gtag is never
111
+ * defined, and consent is still tracked — it gates the attribution
112
+ * capture and PostHog either way.
113
+ */
114
+ if (typeof window.gtag !== 'function') {
115
+ return;
116
+ }
117
+
118
+ window.gtag('consent', mode, {
119
+ ad_storage: granted ? 'granted' : 'denied',
120
+ ad_user_data: granted ? 'granted' : 'denied',
121
+ ad_personalization: granted ? 'granted' : 'denied',
122
+ analytics_storage: granted ? 'granted' : 'denied',
123
+ functionality_storage: granted ? 'granted' : 'denied',
124
+ personalization_storage: granted ? 'granted' : 'denied',
125
+ security_storage: 'granted',
126
+ wait_for_update: 500
127
+ });
128
+ }
129
+
130
+ pushConsentSignal('default', false);
131
+
132
+ // An earlier visit already answered: tell Google before the tag loads.
133
+ if (isGranted()) {
134
+ pushConsentSignal('update', true);
135
+ }
136
+
137
+ function set(granted) {
138
+ try {
139
+ window.localStorage.setItem(STORAGE_KEY, granted ? 'true' : 'false');
140
+ } catch (e) {
141
+ // Nothing to persist to. The in-page decision still applies.
142
+ }
143
+
144
+ pushConsentSignal('update', granted);
145
+
146
+ for (var i = 0; i < listeners.length; i++) {
147
+ try {
148
+ listeners[i](granted);
149
+ } catch (e) {
150
+ // One bad listener must not stop the others.
151
+ }
152
+ }
153
+ }
154
+
155
+ return {
156
+ state: state,
157
+ isGranted: isGranted,
158
+ grant: function () { set(true); },
159
+ deny: function () { set(false); },
160
+ onChange: function (listener) {
161
+ if (typeof listener === 'function') {
162
+ listeners.push(listener);
163
+ }
164
+ }
165
+ };
166
+ })();
167
+ </script>
168
+
169
+ <!--
170
+ Attribution capture (UTM parameters + ad platform click IDs), and the
171
+ readers that hand it on to the two places it has to reach: the signup form
172
+ (through localStorage, same origin) and a Cal.com booking (through the
173
+ embed's metadata).
174
+
175
+ STORAGE MODEL
176
+
177
+ - Last-touch UTM values live in the individual utm* localStorage keys, read
178
+ by the signup page and stored on the User record.
179
+ - Last-touch click IDs live as a JSON object under 'clickIds'.
180
+ - The visitor's first attributed visit is preserved as JSON under
181
+ 'firstTouch' and never overwritten.
182
+
183
+ A visit counts as a new "touch" if it carries ANY utm parameter or ad click
184
+ ID — Google Ads auto-tagging sends only gclid, no UTMs.
185
+
186
+ CONSENT
187
+
188
+ Nothing is written without consent. Attribution seen before the visitor has
189
+ answered the banner is held in memory and flushed if they accept, so the
190
+ ordinary case — land on an ad, accept, sign up — keeps everything. An
191
+ explicit refusal clears anything a previous visit stored.
192
+ -->
193
+ <script>
194
+ (function () {
195
+ var UTM_KEY_MAP = {
196
+ 'utm_source': 'utmSource',
197
+ 'utm_medium': 'utmMedium',
198
+ 'utm_campaign': 'utmCampaign',
199
+ 'utm_term': 'utmTerm',
200
+ 'utm_content': 'utmContent'
201
+ };
202
+
203
+ /*
204
+ * Must stay in step with AdClickIdKeys in
205
+ * Common/Types/Marketing/Attribution.ts — the server whitelists on that
206
+ * list, so a key sent from here that is missing there is silently
207
+ * dropped on arrival.
208
+ */
209
+ var CLICK_ID_PARAMS = ['gclid', 'wbraid', 'gbraid', 'fbclid', 'msclkid', 'li_fat_id', 'twclid', 'rdt_cid'];
210
+
211
+ // Mirrors MaxAttributionValueLength on the server.
212
+ var MAX_VALUE_LENGTH = 500;
213
+
214
+ // Mirrors MAX_FIRST_TOUCH_LENGTH in App/API/CalWebhook.ts.
215
+ var MAX_FIRST_TOUCH_LENGTH = 4000;
216
+
217
+ // Attribution seen on this page but not yet allowed to be stored.
218
+ var pending = null;
219
+
220
+ function canStore() {
221
+ return Boolean(window.oneUptimeConsent && window.oneUptimeConsent.isGranted());
222
+ }
223
+
224
+ function setItem(key, value) {
225
+ try {
226
+ window.localStorage.setItem(key, value);
227
+ } catch (e) {
228
+ // Storage unavailable; attribution is best-effort.
229
+ }
230
+ }
231
+
232
+ function removeItem(key) {
233
+ try {
234
+ window.localStorage.removeItem(key);
235
+ } catch (e) {
236
+ // Nothing to remove from.
237
+ }
238
+ }
239
+
240
+ function getItem(key) {
241
+ try {
242
+ return window.localStorage.getItem(key);
243
+ } catch (e) {
244
+ return null;
245
+ }
246
+ }
247
+
248
+ function bound(value) {
249
+ return String(value).slice(0, MAX_VALUE_LENGTH);
250
+ }
251
+
252
+ function readTouchFromUrl() {
253
+ var urlParams = new URLSearchParams(window.location.search);
254
+
255
+ var utmValues = {};
256
+ var hasUtm = false;
257
+ Object.keys(UTM_KEY_MAP).forEach(function (param) {
258
+ var value = urlParams.get(param);
259
+ if (value) {
260
+ utmValues[UTM_KEY_MAP[param]] = bound(value);
261
+ hasUtm = true;
262
+ }
263
+ });
264
+
265
+ var clickIds = {};
266
+ var hasClickId = false;
267
+ CLICK_ID_PARAMS.forEach(function (param) {
268
+ var value = urlParams.get(param);
269
+ if (value) {
270
+ clickIds[param] = bound(value);
271
+ hasClickId = true;
272
+ }
273
+ });
274
+
275
+ if (!hasUtm && !hasClickId) {
276
+ return null;
277
+ }
278
+
279
+ return {
280
+ utmValues: utmValues,
281
+ clickIds: clickIds,
282
+ hasClickId: hasClickId,
283
+ url: window.location.href,
284
+ referrer: document.referrer || '',
285
+ timestamp: new Date().toISOString()
286
+ };
287
+ }
288
+
289
+ function storeTouch(touch) {
290
+ // New touch: clear previous last-touch values, then store this visit's.
291
+ Object.keys(UTM_KEY_MAP).forEach(function (param) {
292
+ removeItem(UTM_KEY_MAP[param]);
293
+ });
294
+ Object.keys(touch.utmValues).forEach(function (key) {
295
+ setItem(key, touch.utmValues[key]);
296
+ });
297
+ setItem('utmUrl', touch.url);
298
+
299
+ if (touch.hasClickId) {
300
+ setItem('clickIds', JSON.stringify(touch.clickIds));
301
+ } else {
302
+ removeItem('clickIds');
303
+ }
304
+
305
+ // Preserve the first attributed touch forever.
306
+ if (!getItem('firstTouch')) {
307
+ var firstTouch = {};
308
+ Object.keys(touch.utmValues).forEach(function (key) {
309
+ firstTouch[key] = touch.utmValues[key];
310
+ });
311
+ if (touch.hasClickId) {
312
+ firstTouch.clickIds = touch.clickIds;
313
+ }
314
+ firstTouch.landingUrl = touch.url;
315
+ firstTouch.referrer = touch.referrer;
316
+ firstTouch.timestamp = touch.timestamp;
317
+ setItem('firstTouch', JSON.stringify(firstTouch));
318
+ }
319
+ }
320
+
321
+ function clearStoredAttribution() {
322
+ Object.keys(UTM_KEY_MAP).forEach(function (param) {
323
+ removeItem(UTM_KEY_MAP[param]);
324
+ });
325
+ removeItem('utmUrl');
326
+ removeItem('clickIds');
327
+ removeItem('firstTouch');
328
+ }
329
+
330
+ function parseStoredJson(key) {
331
+ var raw = getItem(key);
332
+ if (!raw) { return null; }
333
+ try {
334
+ var parsed = JSON.parse(raw);
335
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
336
+ } catch (e) {
337
+ return null;
338
+ }
339
+ }
340
+
341
+ /*
342
+ * Everything known about where this visitor came from, whether it has
343
+ * been written to storage yet or is still only in memory.
344
+ *
345
+ * The in-memory touch wins: it is this page load, and the stored copy
346
+ * is at best the same thing and at worst an older visit.
347
+ */
348
+ window.oneUptimeGetAttribution = function () {
349
+ var utm = {};
350
+ Object.keys(UTM_KEY_MAP).forEach(function (param) {
351
+ var key = UTM_KEY_MAP[param];
352
+ var value = (pending && pending.utmValues[key]) || getItem(key);
353
+ if (value && value !== 'null' && value !== 'undefined') {
354
+ utm[key] = value;
355
+ }
356
+ });
357
+
358
+ var utmUrl = (pending && pending.url) || getItem('utmUrl');
359
+ if (utmUrl) {
360
+ utm.utmUrl = utmUrl;
361
+ }
362
+
363
+ var clickIds = (pending && pending.hasClickId ? pending.clickIds : null) || parseStoredJson('clickIds') || {};
364
+
365
+ var firstTouch = parseStoredJson('firstTouch');
366
+
367
+ if (!firstTouch && pending) {
368
+ firstTouch = {};
369
+ Object.keys(pending.utmValues).forEach(function (key) {
370
+ firstTouch[key] = pending.utmValues[key];
371
+ });
372
+ if (pending.hasClickId) {
373
+ firstTouch.clickIds = pending.clickIds;
374
+ }
375
+ firstTouch.landingUrl = pending.url;
376
+ firstTouch.referrer = pending.referrer;
377
+ firstTouch.timestamp = pending.timestamp;
378
+ }
379
+
380
+ return {
381
+ utm: utm,
382
+ clickIds: clickIds,
383
+ firstTouch: firstTouch
384
+ };
385
+ };
386
+
387
+ /*
388
+ * The same attribution, flattened into the scalar string map Cal.com
389
+ * accepts as booking metadata.
390
+ *
391
+ * Keys are the snake_case wire spelling the server parses in
392
+ * App/API/CalWebhook.ts. The first touch is a nested object, and Cal
393
+ * metadata values are scalars, so it travels as one JSON string under
394
+ * ou_first_touch — which is exactly what the webhook expects to parse.
395
+ *
396
+ * Returns {} when there is nothing to attribute, so a caller can spread
397
+ * it unconditionally without sending an empty metadata bag.
398
+ */
399
+ /*
400
+ * `bookingKind` says which conversation this embed books. All three
401
+ * embeds use the same Cal event type, so without it the webhook cannot
402
+ * tell a support call from an enterprise demo. It travels in the same
403
+ * bracketed metadata bag as the attribution and is read back by
404
+ * App/API/CalWebhook.ts, which allowlists the value.
405
+ */
406
+ window.oneUptimeCalAttributionMetadata = function (bookingKind) {
407
+ var attribution = window.oneUptimeGetAttribution();
408
+ var config = {};
409
+
410
+ /*
411
+ * Cal takes booking metadata as FLAT, BRACKETED keys on the embed
412
+ * config — `metadata[utm_source]` — and hands them back on the
413
+ * webhook as payload.metadata.utm_source.
414
+ *
415
+ * Not as a nested `metadata: { utm_source: ... }` object. Cal
416
+ * serialises each config value into a query parameter, so a nested
417
+ * object becomes the string "[object Object]" and every key inside
418
+ * it is lost. Nothing errors; bookings keep being recorded, they
419
+ * just arrive attributable to nothing — which is precisely the bug
420
+ * this whole path exists to fix, so getting the shape wrong here
421
+ * would have been indistinguishable from not having done the work.
422
+ *
423
+ * https://cal.com/help/embedding/prefill-booking-form-embed
424
+ */
425
+ function put(key, value, maxLength) {
426
+ if (value) {
427
+ config['metadata[' + key + ']'] =
428
+ String(value).slice(0, maxLength || MAX_VALUE_LENGTH);
429
+ }
430
+ }
431
+
432
+ Object.keys(UTM_KEY_MAP).forEach(function (param) {
433
+ put(param, attribution.utm[UTM_KEY_MAP[param]]);
434
+ });
435
+
436
+ put('utm_url', attribution.utm.utmUrl);
437
+
438
+ Object.keys(attribution.clickIds).forEach(function (param) {
439
+ put(param, attribution.clickIds[param]);
440
+ });
441
+
442
+ if (attribution.firstTouch) {
443
+ var serialized = JSON.stringify(attribution.firstTouch);
444
+ /*
445
+ * The first touch is JSON, so it gets the JSON bound, NOT the
446
+ * 500-char per-value one. A realistic first touch — five UTM
447
+ * values, a click id, and a landing URL that repeats them all
448
+ * as query parameters — runs past 500 characters easily, and
449
+ * truncating JSON produces a string the webhook's JSON.parse
450
+ * rejects. It would drop the whole first touch, silently.
451
+ *
452
+ * Mirrors MAX_FIRST_TOUCH_LENGTH in App/API/CalWebhook.ts.
453
+ */
454
+ if (serialized.length <= MAX_FIRST_TOUCH_LENGTH) {
455
+ put('ou_first_touch', serialized, MAX_FIRST_TOUCH_LENGTH);
456
+ }
457
+ }
458
+
459
+ /*
460
+ * Unconditional, unlike everything above it. The attribution keys
461
+ * describe the visitor and are absent when there is nothing to say
462
+ * about them; the booking kind describes the embed, which always
463
+ * knows what it is. A booking from a visitor who refused
464
+ * attribution storage still arrives knowing it was a demo.
465
+ */
466
+ if (bookingKind) {
467
+ config['metadata[ou_booking_kind]'] = String(bookingKind).slice(0, MAX_VALUE_LENGTH);
468
+ }
469
+
470
+ return config;
471
+ };
472
+
473
+ var touch = readTouchFromUrl();
474
+
475
+ if (touch) {
476
+ if (canStore()) {
477
+ storeTouch(touch);
478
+ } else {
479
+ /*
480
+ * Held, not dropped. A visitor who accepts the banner on this
481
+ * very page — the ordinary path for someone arriving from an
482
+ * ad — keeps the click id that brought them here.
483
+ */
484
+ pending = touch;
485
+ }
486
+ }
487
+
488
+ if (window.oneUptimeConsent) {
489
+ window.oneUptimeConsent.onChange(function (granted) {
490
+ if (granted) {
491
+ if (pending) {
492
+ storeTouch(pending);
493
+ }
494
+ return;
495
+ }
496
+
497
+ // Refused: forget what earlier visits stored, and this page's touch.
498
+ pending = null;
499
+ clearStoredAttribution();
500
+ });
501
+ }
502
+
503
+ /*
504
+ * Reporting is the including page's business, not this partial's.
505
+ *
506
+ * The marketing site emits a `home/`-prefixed PostHog event from here;
507
+ * the product origins emit nothing, and must not start emitting under
508
+ * a `home/` name just because they now share the capture code. So the
509
+ * touch is handed out and the page decides what, if anything, to do
510
+ * with it.
511
+ *
512
+ * Wire keys (utm_source), not property keys (utmSource): this is what
513
+ * arrived on the URL.
514
+ */
515
+ document.addEventListener('DOMContentLoaded', function () {
516
+ if (!touch || typeof window.oneUptimeOnAttributionCaptured !== 'function') {
517
+ return;
518
+ }
519
+
520
+ var eventProps = { utm_url: touch.url };
521
+ Object.keys(UTM_KEY_MAP).forEach(function (param) {
522
+ if (touch.utmValues[UTM_KEY_MAP[param]]) {
523
+ eventProps[param] = touch.utmValues[UTM_KEY_MAP[param]];
524
+ }
525
+ });
526
+ Object.keys(touch.clickIds).forEach(function (param) {
527
+ eventProps[param] = touch.clickIds[param];
528
+ });
529
+
530
+ window.oneUptimeOnAttributionCaptured(eventProps);
531
+ });
532
+ })();
533
+ </script>