@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.
- package/Server/API/EnterpriseLicenseAPI.ts +94 -19
- package/Server/EnvironmentConfig.ts +17 -0
- package/Server/Services/ProjectService.ts +104 -0
- package/Server/Utils/Response.ts +2 -2
- package/Server/Utils/StartServer.ts +2 -2
- package/Server/Views/Partials/AnalyticsConsent.ejs +533 -0
- package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +302 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
- package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
- package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
- package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
- package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
- package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
- package/Tests/UI/Components/Charts/TooltipEntries.test.ts +155 -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/Types/Analytics/RevenueEvent.ts +1 -0
- package/Types/Marketing/MarketingEvent.ts +49 -6
- package/UI/Components/Charts/Area/AreaChart.tsx +8 -1
- package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +19 -4
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +20 -2
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +19 -4
- package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +105 -0
- package/UI/Components/Charts/Line/LineChart.tsx +8 -1
- package/UI/Components/Detail/Detail.tsx +18 -1
- package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
- 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 +15 -0
- package/build/dist/Server/EnvironmentConfig.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/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/Types/Analytics/RevenueEvent.js +1 -0
- package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +40 -1
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +9 -1
- 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/ChartLibrary/AreaChart/AreaChart.js +22 -13
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +27 -15
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +27 -18
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +71 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +9 -1
- 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/package.json +1 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import EnterpriseLicense from "../../Models/DatabaseModels/EnterpriseLicense";
|
|
2
2
|
import EnterpriseLicenseInstance from "../../Models/DatabaseModels/EnterpriseLicenseInstance";
|
|
3
3
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
4
|
-
import { JSONObject } from "../../Types/JSON";
|
|
5
4
|
import PartialEntity from "../../Types/Database/PartialEntity";
|
|
6
5
|
import EnterpriseLicenseInstanceSummary from "../../Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary";
|
|
7
6
|
import EnterpriseLicenseUsageUtil from "../../Utils/EnterpriseLicense/EnterpriseLicenseUsage";
|
|
@@ -26,6 +25,20 @@ import {
|
|
|
26
25
|
import BaseAPI from "./BaseAPI";
|
|
27
26
|
// import { Host } from "../EnvironmentConfig";
|
|
28
27
|
|
|
28
|
+
/*
|
|
29
|
+
* The license state every installation of a license key mirrors locally.
|
|
30
|
+
* Returned by both /validate and /report-user-count so that the daily report
|
|
31
|
+
* keeps a self-hosted installation current on its own.
|
|
32
|
+
*/
|
|
33
|
+
export interface LicenseTerms {
|
|
34
|
+
companyName: string;
|
|
35
|
+
// ISO 8601, or null for a license with no expiry set.
|
|
36
|
+
expiresAt: string | null;
|
|
37
|
+
licenseKey: string;
|
|
38
|
+
// Null means the license carries no seat limit.
|
|
39
|
+
userLimit: number | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
export interface LicenseInstanceUpsert {
|
|
30
43
|
licenseId: ObjectID;
|
|
31
44
|
instanceId: string;
|
|
@@ -137,24 +150,15 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
|
|
|
137
150
|
const instances: Array<EnterpriseLicenseInstance> =
|
|
138
151
|
await this.findLicenseInstances(license.id!);
|
|
139
152
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
licenseKey: license.licenseKey || "",
|
|
144
|
-
userLimit:
|
|
145
|
-
typeof license.userLimit === "number" ? license.userLimit : null,
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const token: string = JSONWebToken.signJsonPayload(
|
|
149
|
-
payload,
|
|
150
|
-
Math.max(secondsUntilExpiry, 1),
|
|
151
|
-
);
|
|
153
|
+
const terms: LicenseTerms = this.getLicenseTerms(license);
|
|
154
|
+
|
|
155
|
+
const token: string | null = this.signLicenseToken(license);
|
|
152
156
|
|
|
153
157
|
return Response.sendJsonObjectResponse(req, res, {
|
|
154
|
-
companyName:
|
|
155
|
-
expiresAt:
|
|
156
|
-
licenseKey:
|
|
157
|
-
userLimit:
|
|
158
|
+
companyName: terms.companyName,
|
|
159
|
+
expiresAt: terms.expiresAt,
|
|
160
|
+
licenseKey: terms.licenseKey,
|
|
161
|
+
userLimit: terms.userLimit,
|
|
158
162
|
currentUserCount:
|
|
159
163
|
typeof license.currentUserCount === "number"
|
|
160
164
|
? license.currentUserCount
|
|
@@ -202,8 +206,19 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
|
|
|
202
206
|
query: {
|
|
203
207
|
licenseKey: licenseKey,
|
|
204
208
|
},
|
|
209
|
+
/*
|
|
210
|
+
* The full set of terms, not just the usage columns: this
|
|
211
|
+
* response is what a self-hosted installation mirrors into its
|
|
212
|
+
* own GlobalConfig every day, so anything it is missing here is
|
|
213
|
+
* a field the customer keeps the stale value of until somebody
|
|
214
|
+
* re-types the license key by hand. Same indexed lookup either
|
|
215
|
+
* way.
|
|
216
|
+
*/
|
|
205
217
|
select: {
|
|
206
218
|
_id: true,
|
|
219
|
+
companyName: true,
|
|
220
|
+
expiresAt: true,
|
|
221
|
+
licenseKey: true,
|
|
207
222
|
userLimit: true,
|
|
208
223
|
isEvaluationLicense: true,
|
|
209
224
|
},
|
|
@@ -282,13 +297,25 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
|
|
|
282
297
|
});
|
|
283
298
|
}
|
|
284
299
|
|
|
300
|
+
const terms: LicenseTerms = this.getLicenseTerms(license);
|
|
301
|
+
|
|
285
302
|
return Response.sendJsonObjectResponse(req, res, {
|
|
303
|
+
companyName: terms.companyName,
|
|
304
|
+
expiresAt: terms.expiresAt,
|
|
305
|
+
licenseKey: terms.licenseKey,
|
|
306
|
+
userLimit: terms.userLimit,
|
|
286
307
|
currentUserCount: currentUserCount,
|
|
287
308
|
userCountUpdatedAt: reportedAt.toISOString(),
|
|
288
|
-
userLimit:
|
|
289
|
-
typeof license.userLimit === "number" ? license.userLimit : null,
|
|
290
309
|
isEvaluationLicense: Boolean(license.isEvaluationLicense),
|
|
291
310
|
instances: this.getInstanceSummaries(instances),
|
|
311
|
+
/*
|
|
312
|
+
* Null once the license has expired. Unlike /validate this route
|
|
313
|
+
* does not reject an expired license - the instance is still
|
|
314
|
+
* expected to report, and the expiry notification emails are built
|
|
315
|
+
* from what it reports - but it must not keep being handed a fresh
|
|
316
|
+
* token, and the expiresAt above is what tells it the truth.
|
|
317
|
+
*/
|
|
318
|
+
token: this.signLicenseToken(license),
|
|
292
319
|
});
|
|
293
320
|
} catch (err) {
|
|
294
321
|
next(err);
|
|
@@ -297,6 +324,54 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
|
|
|
297
324
|
);
|
|
298
325
|
}
|
|
299
326
|
|
|
327
|
+
/*
|
|
328
|
+
* The license terms a self-hosted installation mirrors locally. Built in one
|
|
329
|
+
* place so /validate and /report-user-count cannot drift: the seat limit was
|
|
330
|
+
* missing from what an instance could refresh for exactly as long as the two
|
|
331
|
+
* responses were written out by hand, separately.
|
|
332
|
+
*/
|
|
333
|
+
private getLicenseTerms(license: EnterpriseLicense): LicenseTerms {
|
|
334
|
+
return {
|
|
335
|
+
companyName: license.companyName || "",
|
|
336
|
+
expiresAt: license.expiresAt ? license.expiresAt.toISOString() : null,
|
|
337
|
+
licenseKey: license.licenseKey || "",
|
|
338
|
+
userLimit:
|
|
339
|
+
typeof license.userLimit === "number" ? license.userLimit : null,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/*
|
|
344
|
+
* Signed with the license server's own secret, so it is opaque to the
|
|
345
|
+
* installation holding it - it is the proof that oneuptime.com recognized
|
|
346
|
+
* the key, and the instance only checks that it exists. An expired license
|
|
347
|
+
* gets none, which is why this returns null rather than a short-lived token.
|
|
348
|
+
*/
|
|
349
|
+
private signLicenseToken(license: EnterpriseLicense): string | null {
|
|
350
|
+
if (!license.expiresAt) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const secondsUntilExpiry: number = Math.floor(
|
|
355
|
+
(license.expiresAt.getTime() - Date.now()) / 1000,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
if (secondsUntilExpiry <= 0) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const terms: LicenseTerms = this.getLicenseTerms(license);
|
|
363
|
+
|
|
364
|
+
return JSONWebToken.signJsonPayload(
|
|
365
|
+
{
|
|
366
|
+
companyName: terms.companyName,
|
|
367
|
+
expiresAt: terms.expiresAt,
|
|
368
|
+
licenseKey: terms.licenseKey,
|
|
369
|
+
userLimit: terms.userLimit,
|
|
370
|
+
},
|
|
371
|
+
Math.max(secondsUntilExpiry, 1),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
300
375
|
private parseShortText(value: unknown): string {
|
|
301
376
|
if (typeof value !== "string") {
|
|
302
377
|
return "";
|
|
@@ -368,6 +368,23 @@ export const SubscriptionPlans: Array<SubscriptionPlan> =
|
|
|
368
368
|
export const AnalyticsKey: string = process.env["ANALYTICS_KEY"] || "";
|
|
369
369
|
export const AnalyticsHost: string = process.env["ANALYTICS_HOST"] || "";
|
|
370
370
|
|
|
371
|
+
/*
|
|
372
|
+
* Whether this deployment loads the Google Tag Manager container and pushes
|
|
373
|
+
* marketing conversions into it.
|
|
374
|
+
*
|
|
375
|
+
* Billing being enabled is necessary — a self-hosted install must never load
|
|
376
|
+
* googletagmanager.com — but it is not sufficient. CI runs the SaaS end-to-end
|
|
377
|
+
* suite with BILLING_ENABLED forced to true, and those scripted registrations
|
|
378
|
+
* fire a real `sign_up` into the production container: the key-events trigger
|
|
379
|
+
* matches on the event name alone, with no hostname condition, and GA4 data
|
|
380
|
+
* filters do not apply retroactively. This is the second condition, so an
|
|
381
|
+
* environment can keep billing on and still stay out of the container.
|
|
382
|
+
*
|
|
383
|
+
* Defaults to on, so the hosted product needs no deploy change.
|
|
384
|
+
*/
|
|
385
|
+
export const GoogleTagManagerEnabled: boolean =
|
|
386
|
+
IsBillingEnabled && process.env["GOOGLE_TAG_MANAGER_ENABLED"] !== "false";
|
|
387
|
+
|
|
371
388
|
/*
|
|
372
389
|
* Shared secret Cal.com signs booking webhooks with (App/API/CalWebhook.ts).
|
|
373
390
|
* Server-only — must never be added to FRONTEND_ENV_ALLOW_LIST. Empty means
|
|
@@ -897,6 +897,98 @@ These are no longer recorded against the project and have to be cancelled by han
|
|
|
897
897
|
);
|
|
898
898
|
}
|
|
899
899
|
|
|
900
|
+
/*
|
|
901
|
+
* The first-subscription conversion.
|
|
902
|
+
*
|
|
903
|
+
* A project created directly on a paid plan never passes through changePlan,
|
|
904
|
+
* so the plan-change pass below never sees it — and could not report it if
|
|
905
|
+
* it did, because direction is decided by plan ORDER and a first plan has no
|
|
906
|
+
* previous tier to compare against. That left the most bid-worthy self-serve
|
|
907
|
+
* conversion there is emitting nothing at all: a visitor could click an ad,
|
|
908
|
+
* sign up, pick Growth and pay, and no channel carried a single event.
|
|
909
|
+
*
|
|
910
|
+
* It is its own event rather than an upgrade with an empty old_plan because
|
|
911
|
+
* this is new business and an upgrade is expansion. Mixing them is a
|
|
912
|
+
* distinction nothing downstream could restore.
|
|
913
|
+
*
|
|
914
|
+
* Free projects reach here too — a project on the free plan is still
|
|
915
|
+
* subscribed at the payment provider — so the paid check is what keeps this
|
|
916
|
+
* a revenue event.
|
|
917
|
+
*/
|
|
918
|
+
private captureSubscriptionStartedAnalytics(data: {
|
|
919
|
+
project: Model;
|
|
920
|
+
plan: SubscriptionPlan;
|
|
921
|
+
planId: string;
|
|
922
|
+
seats: number;
|
|
923
|
+
}): void {
|
|
924
|
+
if (!data.project.createdOwnerEmail) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Per-month amount, or null when unknown (custom pricing / unknown plan).
|
|
929
|
+
const monthlyAmountInUSD: number | null = data.plan.isCustomPricing()
|
|
930
|
+
? null
|
|
931
|
+
: data.plan.getYearlyPlanId() === data.planId
|
|
932
|
+
? data.plan.getYearlySubscriptionAmountInUSD()
|
|
933
|
+
: data.plan.getMonthlySubscriptionAmountInUSD();
|
|
934
|
+
|
|
935
|
+
const planIsPaid: boolean =
|
|
936
|
+
data.plan.isCustomPricing() ||
|
|
937
|
+
(monthlyAmountInUSD !== null && monthlyAmountInUSD > 0);
|
|
938
|
+
|
|
939
|
+
if (!planIsPaid) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const properties: JSONObject = {
|
|
944
|
+
project_id: data.project.id?.toString() || "",
|
|
945
|
+
new_plan: data.plan.getName(),
|
|
946
|
+
seats: data.seats,
|
|
947
|
+
/*
|
|
948
|
+
* Always true. The project went straight from not existing to paying,
|
|
949
|
+
* which is the acquisition this event exists to report.
|
|
950
|
+
*/
|
|
951
|
+
is_paid_conversion: true,
|
|
952
|
+
has_custom_pricing: data.plan.isCustomPricing(),
|
|
953
|
+
utm_source: data.project.utmSource || "",
|
|
954
|
+
utm_medium: data.project.utmMedium || "",
|
|
955
|
+
utm_campaign: data.project.utmCampaign || "",
|
|
956
|
+
utm_term: data.project.utmTerm || "",
|
|
957
|
+
utm_content: data.project.utmContent || "",
|
|
958
|
+
click_ids: data.project.clickIds || {},
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
if (monthlyAmountInUSD !== null) {
|
|
962
|
+
properties["new_monthly_amount_in_usd"] = monthlyAmountInUSD;
|
|
963
|
+
// Value for ROAS reporting: monthly recurring revenue at the start.
|
|
964
|
+
properties["value"] = monthlyAmountInUSD * data.seats;
|
|
965
|
+
properties["currency"] = "USD";
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
ProductAnalytics.capture({
|
|
969
|
+
event: "server/subscription_started",
|
|
970
|
+
distinctId: data.project.createdOwnerEmail.toString(),
|
|
971
|
+
properties: properties,
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
MarketingEventUtil.emitInBackground(
|
|
975
|
+
MarketingEventUtil.buildEvent({
|
|
976
|
+
eventType: MarketingEventType.SubscriptionStarted,
|
|
977
|
+
/*
|
|
978
|
+
* Naturally unique: a project has exactly one first subscription, so a
|
|
979
|
+
* queue retry carries the identical id and the receiver dedupes it.
|
|
980
|
+
* Unlike a plan change, this needs no timestamp to separate it from
|
|
981
|
+
* the next one — there is no next one.
|
|
982
|
+
*/
|
|
983
|
+
eventId: `${MarketingEventType.SubscriptionStarted}:${data.project.id?.toString()}`,
|
|
984
|
+
occurredAt: data.project.createdAt || new Date(),
|
|
985
|
+
email: data.project.createdOwnerEmail.toString(),
|
|
986
|
+
attributionSource: data.project,
|
|
987
|
+
data: properties,
|
|
988
|
+
}),
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
|
|
900
992
|
/*
|
|
901
993
|
* The paid-conversion event for ad platforms: fires server-side (immune to
|
|
902
994
|
* ad blockers) whenever a project's subscription plan changes, with enough
|
|
@@ -1286,6 +1378,18 @@ These are no longer recorded against the project and have to be cancelled by han
|
|
|
1286
1378
|
},
|
|
1287
1379
|
});
|
|
1288
1380
|
|
|
1381
|
+
/*
|
|
1382
|
+
* Raised after the row is written, mirroring changePlan: the write is
|
|
1383
|
+
* what makes the subscription real to OneUptime, and the conversion
|
|
1384
|
+
* describes a subscription that exists.
|
|
1385
|
+
*/
|
|
1386
|
+
this.captureSubscriptionStartedAnalytics({
|
|
1387
|
+
project: createdItem,
|
|
1388
|
+
plan: plan,
|
|
1389
|
+
planId: createdItem.paymentProviderPlanId!,
|
|
1390
|
+
seats: 1,
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1289
1393
|
// mark the promo code as used it it exists.
|
|
1290
1394
|
|
|
1291
1395
|
if (createdItem.paymentProviderPromoCode) {
|
package/Server/Utils/Response.ts
CHANGED
|
@@ -25,7 +25,7 @@ import MimeType from "../../Types/File/MimeType";
|
|
|
25
25
|
import PositiveNumber from "../../Types/PositiveNumber";
|
|
26
26
|
import Route from "../../Types/API/Route";
|
|
27
27
|
import CaptureSpan from "./Telemetry/CaptureSpan";
|
|
28
|
-
import {
|
|
28
|
+
import { GoogleTagManagerEnabled } from "../EnvironmentConfig";
|
|
29
29
|
|
|
30
30
|
/*
|
|
31
31
|
* Raster image types, which is to say the types that cannot carry script and
|
|
@@ -239,7 +239,7 @@ export default class Response {
|
|
|
239
239
|
* already use; passing the flag explicitly still wins.
|
|
240
240
|
*/
|
|
241
241
|
oneUptimeResponse.render(path, {
|
|
242
|
-
enableGoogleTagManager:
|
|
242
|
+
enableGoogleTagManager: GoogleTagManagerEnabled,
|
|
243
243
|
...vars,
|
|
244
244
|
});
|
|
245
245
|
}
|
|
@@ -3,7 +3,7 @@ import CommonAPI from "../API/Index";
|
|
|
3
3
|
import { StatusAPIOptions } from "../API/StatusAPI";
|
|
4
4
|
import {
|
|
5
5
|
AppVersion,
|
|
6
|
-
|
|
6
|
+
GoogleTagManagerEnabled,
|
|
7
7
|
TrustedProxyHops,
|
|
8
8
|
getFrontendEnvVars,
|
|
9
9
|
} from "../EnvironmentConfig";
|
|
@@ -449,7 +449,7 @@ const init: InitFunction = async (
|
|
|
449
449
|
}
|
|
450
450
|
|
|
451
451
|
return res.render(path.resolve(process.cwd(), "views/index.ejs"), {
|
|
452
|
-
enableGoogleTagManager:
|
|
452
|
+
enableGoogleTagManager: GoogleTagManagerEnabled,
|
|
453
453
|
...variables,
|
|
454
454
|
});
|
|
455
455
|
} catch (err) {
|