Skip to content

Commit ad3f159

Browse files
authored
Merge pull request #410 from prezly/feature/lazy-consent
[DEV-19486] Feature - Lazy consent setup
2 parents 6a97d0e + 09506ca commit ad3f159

6 files changed

Lines changed: 116 additions & 40 deletions

File tree

package-lock.json

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"prettier": "3.5.0",
4747
"rimraf": "3.0.2",
4848
"ts-jest": "28.0.8",
49-
"typescript": "5.4.4"
49+
"typescript": "5.7.3"
5050
},
5151
"optionalDependencies": {
5252
"@nx/nx-linux-x64-gnu": "18.0.4"

packages/analytics-nextjs/src/Analytics.ts

Lines changed: 86 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,42 @@ import type { AnalyticsBrowser, Plugin } from '@segment/analytics-next';
44
import type Plausible from 'plausible-tracker';
55

66
import {
7-
DEFAULT_CONSENT,
87
DEFAULT_PLAUSIBLE_API_HOST,
98
DEFERRED_USER_LOCAL_STORAGE_KEY,
109
NULL_USER,
1110
} from './constants';
11+
import { checkIsConsentEqual } from './lib/compareConsent';
1212
import { getTrackingPermissions } from './lib/getTrackingPermissions';
1313
import { logToConsole, normalizePrezlyMetaPlugin, sendEventToPrezlyPlugin } from './plugins';
1414
import type { Config, Consent, Identity, PrezlyMeta } from './types';
1515

1616
export class Analytics {
17+
/* eslint-disable @typescript-eslint/naming-convention */
1718
private _identity: Identity | undefined;
1819

19-
private meta: PrezlyMeta | undefined;
20+
private _meta: PrezlyMeta | undefined;
21+
/* eslint-enable @typescript-eslint/naming-convention */
2022

21-
public consent: Consent = DEFAULT_CONSENT;
23+
public consent: Consent | undefined = undefined;
2224

2325
public segment: AnalyticsBrowser | undefined;
2426

2527
public plausible: ReturnType<typeof Plausible> | undefined;
2628

2729
private config: Config | undefined;
2830

31+
private setInitialized!: () => void;
32+
2933
private promises: {
3034
segmentInit?: Promise<void>;
3135
plausibleInit?: Promise<void>;
32-
} = {};
36+
loadGoogleAnalytics?: Promise<(analyticsId: string) => void>;
37+
init: Promise<void>;
38+
} = {
39+
init: new Promise((resolve) => {
40+
this.setInitialized = resolve;
41+
}),
42+
};
3343

3444
get identity(): Identity | undefined {
3545
if (this._identity) {
@@ -50,25 +60,35 @@ export class Analytics {
5060
}
5161

5262
get permissions() {
63+
if (typeof this.config === 'undefined' || typeof this.consent === 'undefined') {
64+
throw new Error('Cannot check permissions before analytics initialization');
65+
}
66+
5367
return getTrackingPermissions({
5468
consent: this.consent,
55-
trackingPolicy: this.config!.trackingPolicy,
69+
trackingPolicy: this.config.trackingPolicy,
5670
});
5771
}
5872

59-
get isInitialized() {
60-
return Boolean(this.config);
61-
}
62-
6373
get integrations() {
6474
return {
6575
Prezly: this.permissions.canTrackToPrezly,
6676
'Segment.io': this.permissions.canTrackToSegment,
6777
};
6878
}
6979

70-
public async init(config: Config) {
71-
if (this.isInitialized) {
80+
private checkInitialized() {
81+
const isConfigSet = typeof this.config !== 'undefined';
82+
const isConsentSet = typeof this.consent !== 'undefined';
83+
84+
if (isConfigSet && isConsentSet) {
85+
this.setInitialized();
86+
}
87+
}
88+
89+
public init = async (config: Config) => {
90+
if (this.config) {
91+
// Cannot re-initialize analytics
7292
return;
7393
}
7494

@@ -92,20 +112,21 @@ export class Analytics {
92112
});
93113

94114
if (config.google) {
95-
const { analyticsId } = config.google;
96-
import('./lib/loadGoogleAnalytics').then(({ loadGoogleAnalytics }) => {
97-
loadGoogleAnalytics(analyticsId);
98-
});
115+
this.promises.loadGoogleAnalytics = import('./lib/loadGoogleAnalytics').then(
116+
({ loadGoogleAnalytics }) => loadGoogleAnalytics,
117+
);
99118
}
100119

101120
if (config.consent) {
102121
this.setConsent(config.consent);
103122
}
104123

105124
if (config.meta) {
106-
this.meta = config.meta;
125+
this.setMeta(config.meta);
107126
}
108-
}
127+
128+
this.checkInitialized();
129+
};
109130

110131
private async loadSegment() {
111132
if (this.config?.segment === false) {
@@ -138,13 +159,25 @@ export class Analytics {
138159
);
139160
}
140161

141-
public setMeta(meta: PrezlyMeta) {
142-
this.meta = meta;
162+
public setMeta = (meta: PrezlyMeta) => {
163+
this._meta = meta;
164+
};
165+
166+
private get meta() {
167+
if (!this._meta) {
168+
console.warn('Tracking without Prezly meta being set');
169+
}
170+
171+
return this._meta;
143172
}
144173

145-
public setConsent(consent: Consent) {
146-
if (!this.isInitialized) {
147-
throw new Error('Analytics uninitialized');
174+
public setConsent = (consent: Consent) => {
175+
if (!this.config) {
176+
throw new Error('Cannot set consent before analytics initialization');
177+
}
178+
179+
if (checkIsConsentEqual(consent, this.consent)) {
180+
return;
148181
}
149182

150183
this.consent = consent;
@@ -154,33 +187,50 @@ export class Analytics {
154187
window[`ga-disable-${analyticsId}`] = this.permissions.canTrackToGoogle;
155188
}
156189

190+
this.promises.loadGoogleAnalytics?.then((loadGoogleAnalytics) => {
191+
if (!this.permissions.canTrackToGoogle) {
192+
return;
193+
}
194+
195+
const { analyticsId } = this.config!.google as Exclude<
196+
Config['google'],
197+
false | undefined
198+
>;
199+
200+
loadGoogleAnalytics(analyticsId);
201+
});
202+
157203
this.promises.segmentInit?.then(() => {
158204
if (!this.segment?.instance && this.permissions.canLoadSegment) {
159205
this.loadSegment();
160206
}
161207

162-
if (this.identity && this.permissions.canIdentify) {
163-
const { identity } = this;
208+
const { identity } = this;
209+
if (identity && this.permissions.canIdentify) {
164210
this.segment?.identify(
165211
identity.userId,
166212
{ ...identity.traits, prezly: this.meta },
167213
{ integrations: this.integrations },
168214
);
169215
}
170216
});
171-
}
172217

173-
public async alias(userId: string, previousId: string) {
218+
this.checkInitialized();
219+
};
220+
221+
public alias = async (userId: string, previousId: string) => {
222+
await this.promises.init;
174223
await this.promises.segmentInit;
175224
await this.segment?.alias(userId, previousId, { integrations: this.integrations });
176-
}
225+
};
177226

178-
public async page(
227+
public page = async (
179228
category?: string,
180229
name?: string,
181230
properties: object = {},
182231
callback?: () => void,
183-
) {
232+
) => {
233+
await this.promises.init;
184234
await this.promises.segmentInit;
185235
await this.segment?.page(
186236
category,
@@ -189,11 +239,12 @@ export class Analytics {
189239
{ integrations: this.integrations },
190240
callback,
191241
);
192-
}
242+
};
193243

194-
public async track(event: string, properties: object = {}, callback?: () => void) {
244+
public track = async (event: string, properties: object = {}, callback?: () => void) => {
195245
const props = this.meta ? { ...properties, prezly: this.meta } : properties;
196246

247+
await this.promises.init;
197248
await Promise.all([
198249
this.promises.plausibleInit?.then(() => {
199250
this.plausible?.trackEvent(event, {
@@ -205,11 +256,12 @@ export class Analytics {
205256
this.segment?.track(event, props, { integrations: this.integrations }, callback),
206257
),
207258
]);
208-
}
259+
};
209260

210-
public async identify(userId: string, traits: object = {}, callback?: () => void) {
261+
public identify = async (userId: string, traits: object = {}, callback?: () => void) => {
211262
this.identity = { userId, traits };
212263

264+
await this.promises.init;
213265
await this.promises.segmentInit;
214266

215267
if (this.permissions.canIdentify) {
@@ -220,7 +272,7 @@ export class Analytics {
220272
callback,
221273
);
222274
}
223-
}
275+
};
224276

225277
public user() {
226278
return this.segment?.instance?.user() ?? NULL_USER;

packages/analytics-nextjs/src/constants.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import { type Config, type Consent, TrackingPolicy } from './types';
1+
import { type Config, TrackingPolicy } from './types';
22

33
export const DEFERRED_USER_LOCAL_STORAGE_KEY = 'identity';
44

55
export const DEFAULT_PLAUSIBLE_API_HOST = 'https://atlas.prezly.com/api/event';
66

7-
export const DEFAULT_CONSENT: Consent = { categories: [] };
8-
97
export const DEFAULT_CONFIG: Config = {
108
segment: {
119
settings: {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Consent } from '../types';
2+
3+
export function checkIsConsentEqual(consent1: Consent | undefined, consent2: Consent | undefined) {
4+
if (consent1 === consent2) {
5+
return true;
6+
}
7+
8+
if (typeof consent1 === 'undefined' || typeof consent2 === 'undefined') {
9+
return false;
10+
}
11+
12+
const containsSameCategories =
13+
consent1.categories.length === consent2.categories.length &&
14+
new Set([...consent1.categories, ...consent2.categories]).size ===
15+
consent1.categories.length;
16+
17+
return containsSameCategories;
18+
}

packages/analytics-nextjs/src/lib/loadGoogleAnalytics.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ function loadAnalytics(analyticsId: string) {
3939
}
4040

4141
export function loadGoogleAnalytics(analyticsId: string) {
42+
const isLoaded = Boolean(document.getElementById('google-tag-manager-bootstrap'));
43+
44+
if (isLoaded) {
45+
return;
46+
}
47+
4248
if (analyticsId.startsWith('GTM-')) {
4349
loadTagManager(analyticsId as `GTM-${string}`);
4450
} else {

0 commit comments

Comments
 (0)