@oneuptime/common 12.0.19 → 12.0.21
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/Models/AnalyticsModels/ChangeEvent.ts +331 -0
- package/Models/AnalyticsModels/Index.ts +2 -0
- package/Server/API/EnterpriseLicenseAPI.ts +94 -19
- package/Server/EnvironmentConfig.ts +15 -5
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +0 -2
- package/Server/Services/ChangeEventService.ts +11 -0
- package/Server/Services/EnterpriseLicenseService.ts +3 -3
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/ProjectService.ts +104 -0
- package/Server/Utils/Attribution.ts +11 -11
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
- package/Server/Utils/Response.ts +2 -2
- package/Server/Utils/StartServer.ts +2 -2
- package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +441 -0
- package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +366 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
- package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
- package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
- package/Tests/App/Dashboard/InvestigationArtifacts.test.tsx +315 -0
- package/Tests/App/Dashboard/InvestigationDrawer.test.tsx +258 -0
- package/Tests/App/Dashboard/MetricChartsCompare.test.tsx +171 -0
- package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
- package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
- package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
- package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
- package/Tests/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.test.ts +224 -0
- package/Tests/Server/Utils/Attribution.test.ts +4 -5
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +2 -2
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.test.ts +300 -0
- package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
- package/Tests/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
- package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
- package/Tests/UI/Components/Charts/TooltipEntries.test.ts +191 -0
- package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
- package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
- package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
- package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
- package/Types/Analytics/RevenueEvent.ts +1 -0
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
- package/Types/Marketing/Attribution.ts +9 -15
- package/Types/Marketing/MarketingEvent.ts +19 -12
- package/UI/Components/Charts/Area/AreaChart.tsx +23 -1
- package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +208 -97
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +26 -3
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +232 -118
- package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +120 -0
- package/UI/Components/Charts/Line/LineChart.tsx +23 -1
- package/UI/Components/Detail/Detail.tsx +18 -1
- package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
- package/Utils/Metrics/MetricExplorerUrl.ts +119 -5
- package/Utils/Telemetry/CrossSignalScope.ts +19 -10
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js +298 -0
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/Index.js +2 -0
- package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +14 -5
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +0 -2
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/ChangeEventService.js +9 -0
- package/build/dist/Server/Services/ChangeEventService.js.map +1 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js +3 -3
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +88 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Utils/Attribution.js +6 -6
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +4 -5
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -1
- package/build/dist/Server/Utils/Response.js +2 -2
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +2 -2
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js +150 -0
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js.map +1 -0
- package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
- package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
- package/build/dist/Types/Marketing/Attribution.js +9 -15
- package/build/dist/Types/Marketing/Attribution.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +12 -4
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +10 -2
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +9 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +114 -51
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +35 -23
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +122 -55
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +86 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +10 -2
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +17 -1
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js +81 -0
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +14 -8
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/package.json +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +0 -117
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +0 -106
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +0 -1
|
@@ -0,0 +1,441 @@
|
|
|
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.
|
|
32
|
+
-%>
|
|
33
|
+
<!--
|
|
34
|
+
Consent.
|
|
35
|
+
|
|
36
|
+
This block runs BEFORE Google Tag Manager and before PostHog, because both
|
|
37
|
+
of the things it does are only meaningful ahead of them: Consent Mode's
|
|
38
|
+
defaults have to be on the dataLayer before the container reads it, and
|
|
39
|
+
PostHog must not be loaded at all until there is consent to load it.
|
|
40
|
+
|
|
41
|
+
Three states, not two. `granted` and `denied` are the visitor's explicit
|
|
42
|
+
choices; `unset` is the state before they have answered, and it is treated
|
|
43
|
+
as denied for storage purposes rather than as permission-by-silence.
|
|
44
|
+
|
|
45
|
+
What that costs, stated plainly: a visitor who never touches the banner is
|
|
46
|
+
not measured, and their attribution is not stored. What stops that from
|
|
47
|
+
losing an ad click outright is the pending-attribution buffer further
|
|
48
|
+
down — attribution seen on this page is held in memory and written the
|
|
49
|
+
moment consent is granted, so accepting the banner on the landing page
|
|
50
|
+
keeps everything the click carried.
|
|
51
|
+
-->
|
|
52
|
+
<% if(typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false){ %>
|
|
53
|
+
<!--
|
|
54
|
+
The dataLayer and the gtag shim, defined before both the consent signals
|
|
55
|
+
below and the container itself.
|
|
56
|
+
|
|
57
|
+
Deliberately inside the enableGoogleTagManager gate. A self-hosted install
|
|
58
|
+
loads no Google tag at all, and every OneUptime event guards on
|
|
59
|
+
`typeof dataLayer !== 'undefined'` — so creating the array there would turn
|
|
60
|
+
those no-ops into an array that grows for the life of the page and that
|
|
61
|
+
nothing will ever read.
|
|
62
|
+
-->
|
|
63
|
+
<script>
|
|
64
|
+
window.dataLayer = window.dataLayer || [];
|
|
65
|
+
function gtag() { dataLayer.push(arguments); }
|
|
66
|
+
</script>
|
|
67
|
+
<% } %>
|
|
68
|
+
|
|
69
|
+
<script>
|
|
70
|
+
window.oneUptimeConsent = (function () {
|
|
71
|
+
var STORAGE_KEY = 'cookiesAccepted';
|
|
72
|
+
var listeners = [];
|
|
73
|
+
|
|
74
|
+
function readRaw() {
|
|
75
|
+
try {
|
|
76
|
+
return window.localStorage.getItem(STORAGE_KEY);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
/*
|
|
79
|
+
* Private mode, disabled storage, or a cross-origin sandbox.
|
|
80
|
+
* Unreadable storage is not consent.
|
|
81
|
+
*/
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function state() {
|
|
87
|
+
var raw = readRaw();
|
|
88
|
+
if (raw === 'true') { return 'granted'; }
|
|
89
|
+
if (raw === 'false') { return 'denied'; }
|
|
90
|
+
return 'unset';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isGranted() {
|
|
94
|
+
return state() === 'granted';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* Consent Mode v2. `security_storage` is granted because it covers
|
|
99
|
+
* things like fraud prevention that are not analytics; everything that
|
|
100
|
+
* feeds advertising or measurement starts denied.
|
|
101
|
+
*
|
|
102
|
+
* wait_for_update gives the container half a second to see an update
|
|
103
|
+
* before it decides how to behave, which is what makes a visitor who
|
|
104
|
+
* accepts immediately still count.
|
|
105
|
+
*/
|
|
106
|
+
function pushConsentSignal(mode, granted) {
|
|
107
|
+
/*
|
|
108
|
+
* No tag, nothing to tell. On a self-hosted install gtag is never
|
|
109
|
+
* defined, and consent is still tracked — it gates the attribution
|
|
110
|
+
* capture and PostHog either way.
|
|
111
|
+
*/
|
|
112
|
+
if (typeof window.gtag !== 'function') {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
window.gtag('consent', mode, {
|
|
117
|
+
ad_storage: granted ? 'granted' : 'denied',
|
|
118
|
+
ad_user_data: granted ? 'granted' : 'denied',
|
|
119
|
+
ad_personalization: granted ? 'granted' : 'denied',
|
|
120
|
+
analytics_storage: granted ? 'granted' : 'denied',
|
|
121
|
+
functionality_storage: granted ? 'granted' : 'denied',
|
|
122
|
+
personalization_storage: granted ? 'granted' : 'denied',
|
|
123
|
+
security_storage: 'granted',
|
|
124
|
+
wait_for_update: 500
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
pushConsentSignal('default', false);
|
|
129
|
+
|
|
130
|
+
// An earlier visit already answered: tell Google before the tag loads.
|
|
131
|
+
if (isGranted()) {
|
|
132
|
+
pushConsentSignal('update', true);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function set(granted) {
|
|
136
|
+
try {
|
|
137
|
+
window.localStorage.setItem(STORAGE_KEY, granted ? 'true' : 'false');
|
|
138
|
+
} catch (e) {
|
|
139
|
+
// Nothing to persist to. The in-page decision still applies.
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
pushConsentSignal('update', granted);
|
|
143
|
+
|
|
144
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
145
|
+
try {
|
|
146
|
+
listeners[i](granted);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
// One bad listener must not stop the others.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
state: state,
|
|
155
|
+
isGranted: isGranted,
|
|
156
|
+
grant: function () { set(true); },
|
|
157
|
+
deny: function () { set(false); },
|
|
158
|
+
onChange: function (listener) {
|
|
159
|
+
if (typeof listener === 'function') {
|
|
160
|
+
listeners.push(listener);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
})();
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
<!--
|
|
168
|
+
Attribution capture (UTM parameters + ad platform click IDs), and the
|
|
169
|
+
reader that hands it on to the signup form (through localStorage, same
|
|
170
|
+
origin).
|
|
171
|
+
|
|
172
|
+
STORAGE MODEL
|
|
173
|
+
|
|
174
|
+
- Last-touch UTM values live in the individual utm* localStorage keys, read
|
|
175
|
+
by the signup page and stored on the User record.
|
|
176
|
+
- Last-touch click IDs live as a JSON object under 'clickIds'.
|
|
177
|
+
- The visitor's first attributed visit is preserved as JSON under
|
|
178
|
+
'firstTouch' and never overwritten.
|
|
179
|
+
|
|
180
|
+
A visit counts as a new "touch" if it carries ANY utm parameter or ad click
|
|
181
|
+
ID — Google Ads auto-tagging sends only gclid, no UTMs.
|
|
182
|
+
|
|
183
|
+
CONSENT
|
|
184
|
+
|
|
185
|
+
Nothing is written without consent. Attribution seen before the visitor has
|
|
186
|
+
answered the banner is held in memory and flushed if they accept, so the
|
|
187
|
+
ordinary case — land on an ad, accept, sign up — keeps everything. An
|
|
188
|
+
explicit refusal clears anything a previous visit stored.
|
|
189
|
+
-->
|
|
190
|
+
<script>
|
|
191
|
+
(function () {
|
|
192
|
+
var UTM_KEY_MAP = {
|
|
193
|
+
'utm_source': 'utmSource',
|
|
194
|
+
'utm_medium': 'utmMedium',
|
|
195
|
+
'utm_campaign': 'utmCampaign',
|
|
196
|
+
'utm_term': 'utmTerm',
|
|
197
|
+
'utm_content': 'utmContent'
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/*
|
|
201
|
+
* Must stay in step with AdClickIdKeys in
|
|
202
|
+
* Common/Types/Marketing/Attribution.ts — the server whitelists on that
|
|
203
|
+
* list, so a key sent from here that is missing there is silently
|
|
204
|
+
* dropped on arrival.
|
|
205
|
+
*/
|
|
206
|
+
var CLICK_ID_PARAMS = ['gclid', 'wbraid', 'gbraid', 'fbclid', 'msclkid', 'li_fat_id', 'twclid', 'rdt_cid'];
|
|
207
|
+
|
|
208
|
+
// Mirrors MaxAttributionValueLength on the server.
|
|
209
|
+
var MAX_VALUE_LENGTH = 500;
|
|
210
|
+
|
|
211
|
+
// Attribution seen on this page but not yet allowed to be stored.
|
|
212
|
+
var pending = null;
|
|
213
|
+
|
|
214
|
+
function canStore() {
|
|
215
|
+
return Boolean(window.oneUptimeConsent && window.oneUptimeConsent.isGranted());
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function setItem(key, value) {
|
|
219
|
+
try {
|
|
220
|
+
window.localStorage.setItem(key, value);
|
|
221
|
+
} catch (e) {
|
|
222
|
+
// Storage unavailable; attribution is best-effort.
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function removeItem(key) {
|
|
227
|
+
try {
|
|
228
|
+
window.localStorage.removeItem(key);
|
|
229
|
+
} catch (e) {
|
|
230
|
+
// Nothing to remove from.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getItem(key) {
|
|
235
|
+
try {
|
|
236
|
+
return window.localStorage.getItem(key);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function bound(value) {
|
|
243
|
+
return String(value).slice(0, MAX_VALUE_LENGTH);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function readTouchFromUrl() {
|
|
247
|
+
var urlParams = new URLSearchParams(window.location.search);
|
|
248
|
+
|
|
249
|
+
var utmValues = {};
|
|
250
|
+
var hasUtm = false;
|
|
251
|
+
Object.keys(UTM_KEY_MAP).forEach(function (param) {
|
|
252
|
+
var value = urlParams.get(param);
|
|
253
|
+
if (value) {
|
|
254
|
+
utmValues[UTM_KEY_MAP[param]] = bound(value);
|
|
255
|
+
hasUtm = true;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
var clickIds = {};
|
|
260
|
+
var hasClickId = false;
|
|
261
|
+
CLICK_ID_PARAMS.forEach(function (param) {
|
|
262
|
+
var value = urlParams.get(param);
|
|
263
|
+
if (value) {
|
|
264
|
+
clickIds[param] = bound(value);
|
|
265
|
+
hasClickId = true;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (!hasUtm && !hasClickId) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
utmValues: utmValues,
|
|
275
|
+
clickIds: clickIds,
|
|
276
|
+
hasClickId: hasClickId,
|
|
277
|
+
url: window.location.href,
|
|
278
|
+
referrer: document.referrer || '',
|
|
279
|
+
timestamp: new Date().toISOString()
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function storeTouch(touch) {
|
|
284
|
+
// New touch: clear previous last-touch values, then store this visit's.
|
|
285
|
+
Object.keys(UTM_KEY_MAP).forEach(function (param) {
|
|
286
|
+
removeItem(UTM_KEY_MAP[param]);
|
|
287
|
+
});
|
|
288
|
+
Object.keys(touch.utmValues).forEach(function (key) {
|
|
289
|
+
setItem(key, touch.utmValues[key]);
|
|
290
|
+
});
|
|
291
|
+
setItem('utmUrl', touch.url);
|
|
292
|
+
|
|
293
|
+
if (touch.hasClickId) {
|
|
294
|
+
setItem('clickIds', JSON.stringify(touch.clickIds));
|
|
295
|
+
} else {
|
|
296
|
+
removeItem('clickIds');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Preserve the first attributed touch forever.
|
|
300
|
+
if (!getItem('firstTouch')) {
|
|
301
|
+
var firstTouch = {};
|
|
302
|
+
Object.keys(touch.utmValues).forEach(function (key) {
|
|
303
|
+
firstTouch[key] = touch.utmValues[key];
|
|
304
|
+
});
|
|
305
|
+
if (touch.hasClickId) {
|
|
306
|
+
firstTouch.clickIds = touch.clickIds;
|
|
307
|
+
}
|
|
308
|
+
firstTouch.landingUrl = touch.url;
|
|
309
|
+
firstTouch.referrer = touch.referrer;
|
|
310
|
+
firstTouch.timestamp = touch.timestamp;
|
|
311
|
+
setItem('firstTouch', JSON.stringify(firstTouch));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function clearStoredAttribution() {
|
|
316
|
+
Object.keys(UTM_KEY_MAP).forEach(function (param) {
|
|
317
|
+
removeItem(UTM_KEY_MAP[param]);
|
|
318
|
+
});
|
|
319
|
+
removeItem('utmUrl');
|
|
320
|
+
removeItem('clickIds');
|
|
321
|
+
removeItem('firstTouch');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function parseStoredJson(key) {
|
|
325
|
+
var raw = getItem(key);
|
|
326
|
+
if (!raw) { return null; }
|
|
327
|
+
try {
|
|
328
|
+
var parsed = JSON.parse(raw);
|
|
329
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
330
|
+
} catch (e) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/*
|
|
336
|
+
* Everything known about where this visitor came from, whether it has
|
|
337
|
+
* been written to storage yet or is still only in memory.
|
|
338
|
+
*
|
|
339
|
+
* The in-memory touch wins: it is this page load, and the stored copy
|
|
340
|
+
* is at best the same thing and at worst an older visit.
|
|
341
|
+
*/
|
|
342
|
+
window.oneUptimeGetAttribution = function () {
|
|
343
|
+
var utm = {};
|
|
344
|
+
Object.keys(UTM_KEY_MAP).forEach(function (param) {
|
|
345
|
+
var key = UTM_KEY_MAP[param];
|
|
346
|
+
var value = (pending && pending.utmValues[key]) || getItem(key);
|
|
347
|
+
if (value && value !== 'null' && value !== 'undefined') {
|
|
348
|
+
utm[key] = value;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
var utmUrl = (pending && pending.url) || getItem('utmUrl');
|
|
353
|
+
if (utmUrl) {
|
|
354
|
+
utm.utmUrl = utmUrl;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
var clickIds = (pending && pending.hasClickId ? pending.clickIds : null) || parseStoredJson('clickIds') || {};
|
|
358
|
+
|
|
359
|
+
var firstTouch = parseStoredJson('firstTouch');
|
|
360
|
+
|
|
361
|
+
if (!firstTouch && pending) {
|
|
362
|
+
firstTouch = {};
|
|
363
|
+
Object.keys(pending.utmValues).forEach(function (key) {
|
|
364
|
+
firstTouch[key] = pending.utmValues[key];
|
|
365
|
+
});
|
|
366
|
+
if (pending.hasClickId) {
|
|
367
|
+
firstTouch.clickIds = pending.clickIds;
|
|
368
|
+
}
|
|
369
|
+
firstTouch.landingUrl = pending.url;
|
|
370
|
+
firstTouch.referrer = pending.referrer;
|
|
371
|
+
firstTouch.timestamp = pending.timestamp;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
utm: utm,
|
|
376
|
+
clickIds: clickIds,
|
|
377
|
+
firstTouch: firstTouch
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
var touch = readTouchFromUrl();
|
|
382
|
+
|
|
383
|
+
if (touch) {
|
|
384
|
+
if (canStore()) {
|
|
385
|
+
storeTouch(touch);
|
|
386
|
+
} else {
|
|
387
|
+
/*
|
|
388
|
+
* Held, not dropped. A visitor who accepts the banner on this
|
|
389
|
+
* very page — the ordinary path for someone arriving from an
|
|
390
|
+
* ad — keeps the click id that brought them here.
|
|
391
|
+
*/
|
|
392
|
+
pending = touch;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (window.oneUptimeConsent) {
|
|
397
|
+
window.oneUptimeConsent.onChange(function (granted) {
|
|
398
|
+
if (granted) {
|
|
399
|
+
if (pending) {
|
|
400
|
+
storeTouch(pending);
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Refused: forget what earlier visits stored, and this page's touch.
|
|
406
|
+
pending = null;
|
|
407
|
+
clearStoredAttribution();
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/*
|
|
412
|
+
* Reporting is the including page's business, not this partial's.
|
|
413
|
+
*
|
|
414
|
+
* The marketing site emits a `home/`-prefixed PostHog event from here;
|
|
415
|
+
* the product origins emit nothing, and must not start emitting under
|
|
416
|
+
* a `home/` name just because they now share the capture code. So the
|
|
417
|
+
* touch is handed out and the page decides what, if anything, to do
|
|
418
|
+
* with it.
|
|
419
|
+
*
|
|
420
|
+
* Wire keys (utm_source), not property keys (utmSource): this is what
|
|
421
|
+
* arrived on the URL.
|
|
422
|
+
*/
|
|
423
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
424
|
+
if (!touch || typeof window.oneUptimeOnAttributionCaptured !== 'function') {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
var eventProps = { utm_url: touch.url };
|
|
429
|
+
Object.keys(UTM_KEY_MAP).forEach(function (param) {
|
|
430
|
+
if (touch.utmValues[UTM_KEY_MAP[param]]) {
|
|
431
|
+
eventProps[param] = touch.utmValues[UTM_KEY_MAP[param]];
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
Object.keys(touch.clickIds).forEach(function (param) {
|
|
435
|
+
eventProps[param] = touch.clickIds[param];
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
window.oneUptimeOnAttributionCaptured(eventProps);
|
|
439
|
+
});
|
|
440
|
+
})();
|
|
441
|
+
</script>
|