Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TF-633] Feature: migrate to @segment/analytics-next package #1

Merged
merged 16 commits into from
Apr 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17,502 changes: 10,666 additions & 6,836 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"check": "npm run lint && npm run typecheck && npm run test",
"release": "npm run release:prepare && npm run release:publish",
"release:preview": "npm run release:prepare && npm run release:publish:preview",
"release:prepare": "npm run check && npm run build",
"release:prepare": "npm run check",
"release:publish": "lerna publish",
"release:publish:preview": "lerna publish --npm-tag preview --skip-git"
},
Expand All @@ -29,9 +29,7 @@
"url": "https://github.com/prezly/analytics/issues"
},
"homepage": "https://github.com/prezly/analytics#readme",
"workspaces": [
"packages/*"
],
"workspaces": ["packages/*"],
"engines": {
"node": ">= 14.x",
"npm": ">= 7.x"
Expand All @@ -51,6 +49,7 @@
"typescript": "^4.6.3"
},
"dependencies": {
"@prezly/sdk": "^6.21.0",
"next": "^12.1.4",
"react": "^17.0.2",
"react-dom": "^17.0.2"
Expand Down
8 changes: 6 additions & 2 deletions packages/analytics-nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
"main": "build/index.js",
"types": "build/index.d.ts",
"scripts": {
"prebuild": "rimraf build/* *.tsbuildinfo",
"clean": "rimraf build/* *.tsbuildinfo",
"version:output": "echo \"export const version = '\"$npm_package_version\"';\" > src/version.ts",
"prebuild": "npm run clean && npm run version:output",
"version": "npm run build",
"build": "tsc --project .",
"lint": "eslint ./src --ext=.ts,.tsx",
"test": "echo \"Tests? maybe next time ;)\" && exit 0"
Expand All @@ -22,12 +25,13 @@
},
"homepage": "https://github.com/prezly/analytics#readme",
"peerDependencies": {
"@prezly/sdk": "^6.21.0",
"next": "^12.x",
"react": "^17.x",
"react-dom": "^17.x"
},
"dependencies": {
"@prezly/sdk": "^6.17.0",
"@segment/analytics-next": "^1.35.0",
"js-cookie": "^3.0.1",
"react-use": "^17.3.2"
},
Expand Down
81 changes: 55 additions & 26 deletions packages/analytics-nextjs/src/context.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import type { Newsroom, Story } from '@prezly/sdk';
import { TrackingPolicy } from '@prezly/sdk';
import type { Analytics, Plugin } from '@segment/analytics-next';
import { AnalyticsBrowser } from '@segment/analytics-next';
import Head from 'next/head';
import Script from 'next/script';
import type { PropsWithChildren } from 'react';
import { createContext, useContext, useEffect, useState } from 'react';

import {
createAnalyticsStub,
getAnalyticsJsUrl,
getConsentCookie,
isPrezlyTrackingAllowed,
setConsentCookie,
} from './lib';
import { getConsentCookie, isPrezlyTrackingAllowed, setConsentCookie } from './lib';
import { injectPrezlyMetaPlugin, sendEventToPrezlyPlugin } from './plugins';

interface Context {
analytics: Analytics | undefined;
consent: boolean | null;
isAnalyticsReady: boolean;
isEnabled: boolean;
isTrackingAllowed: boolean | null;
newsroom: Newsroom;
Expand All @@ -27,6 +23,7 @@ interface Props {
isEnabled?: boolean;
newsroom: Newsroom;
story: Story | undefined;
plugins?: Plugin[];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it would be a nice feature to allow the package consumers to pass their own plugins, that way they would be able to extend the features without the need to fork the library.

}

export const AnalyticsContext = createContext<Context | undefined>(undefined);
Expand All @@ -45,16 +42,55 @@ export function AnalyticsContextProvider({
isEnabled = true,
newsroom,
story,
plugins,
}: PropsWithChildren<Props>) {
const { uuid, tracking_policy: trackingPolicy } = newsroom;
const [isAnalyticsReady, setAnalyticsReady] = useState(false);
const {
tracking_policy: trackingPolicy,
segment_analytics_id: segmentWriteKey,
uuid,
} = newsroom;
const [consent, setConsent] = useState(getConsentCookie());
const isTrackingAllowed = isEnabled && isPrezlyTrackingAllowed(consent, newsroom);

const [analytics, setAnalytics] = useState<Analytics | undefined>(undefined);

useEffect(() => {
if (trackingPolicy === TrackingPolicy.DISABLED) {
window.analytics = createAnalyticsStub();
async function loadAnalytics(writeKey: string) {
const [response] = await AnalyticsBrowser.load(
{
writeKey,
// If no Segment Write Key is provided, we initialize the library settings manually
...(!writeKey && {
cdnSettings: {
integrations: {},
},
}),
Comment on lines +62 to +67
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code allows the library to initialize without a working Segment Write Key (it is used to fetch the source configuration from Segment, which we don't need to Prezly-only tracking).
Somehow the tracking calls to Segment API still go through even without authorization. This is handled later in the code.

plugins: [
injectPrezlyMetaPlugin(),
sendEventToPrezlyPlugin(uuid),
...(plugins || []),
],
},
{
// By default, the analytics.js library plants its cookies on the top-level domain.
// We need to completely isolate tracking between any Prezly newsroom hosted on a .prezly.com subdomain.
cookie: {
domain: document.location.host,
},
// Disable calls to Segment API completely if no Write Key is provided
...(!writeKey && {
// eslint-disable-next-line @typescript-eslint/naming-convention
integrations: { 'Segment.io': false },
}),
Comment on lines +80 to +84
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After digging through analytics-next source code, I found out that the code sending events to Segment is also a plugin (!), that can be disabled. This was an undocumented feature, but it effectively disables any calls to Segment API, while all the other plugins (like sending events to Prezly) still work fine.

},
);
setAnalytics(response);
}

if (isTrackingAllowed) {
loadAnalytics(segmentWriteKey || '');
}
});
}, [segmentWriteKey, isTrackingAllowed, uuid, plugins]);

useEffect(() => {
if (typeof consent === 'boolean') {
Expand All @@ -65,29 +101,22 @@ export function AnalyticsContextProvider({
return (
<AnalyticsContext.Provider
value={{
analytics,
consent,
isAnalyticsReady,
isEnabled,
isTrackingAllowed: isEnabled && isPrezlyTrackingAllowed(consent, newsroom),
isTrackingAllowed,
newsroom,
setConsent,
trackingPolicy: newsroom.tracking_policy,
trackingPolicy,
}}
>
<Head>
<meta name="prezly:newsroom" content={newsroom.uuid} />
{story && <meta name="prezly:story" content={story.uuid} />}
{newsroom.tracking_policy !== TrackingPolicy.DEFAULT && (
<meta name="prezly:tracking_policy" content={newsroom.tracking_policy} />
{trackingPolicy !== TrackingPolicy.DEFAULT && (
<meta name="prezly:tracking_policy" content={trackingPolicy} />
)}
</Head>
{trackingPolicy !== TrackingPolicy.DISABLED && (
<Script
key="prezly-analytics"
onLoad={() => setAnalyticsReady(true)}
src={getAnalyticsJsUrl(uuid)}
/>
)}
{children}
</AnalyticsContext.Provider>
);
Expand Down
7 changes: 0 additions & 7 deletions packages/analytics-nextjs/src/global.d.ts

This file was deleted.

115 changes: 67 additions & 48 deletions packages/analytics-nextjs/src/hooks/useAnalytics.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TrackingPolicy } from '@prezly/sdk';
import { useCallback, useEffect } from 'react';
import { useLocalStorage, useQueue } from 'react-use';
import { useLatest, useLocalStorage, useQueue } from 'react-use';

import { useAnalyticsContext } from '../context';
import { stringify } from '../lib';
Expand All @@ -9,8 +9,10 @@ import type { DeferredIdentity } from '../types';
const DEFERRED_IDENTITY_STORAGE_KEY = 'prezly_ajs_deferred_identity';

export function useAnalytics() {
const { consent, isAnalyticsReady, isEnabled, newsroom, trackingPolicy } =
useAnalyticsContext();
const { analytics, consent, isEnabled, newsroom, trackingPolicy } = useAnalyticsContext();
// We use ref to `analytics` object, cause our tracking calls are added to the callback queue, and those need to have access to the most recent instance if `analytics`,
// which would not be possible when passing the `analytics` object directly
const analyticsRef = useLatest(analytics);
const [deferredIdentity, setDeferredIdentity, removeDeferredIdentity] =
useLocalStorage<DeferredIdentity>(DEFERRED_IDENTITY_STORAGE_KEY);
const {
Expand Down Expand Up @@ -53,61 +55,77 @@ export function useAnalytics() {
}

addToQueue(() => {
if (window.analytics && window.analytics.identify) {
window.analytics.identify(userId, traits, buildOptions(), callback);
if (analyticsRef.current && analyticsRef.current.identify) {
analyticsRef.current.identify(userId, traits, buildOptions(), callback);
}
});
},
// The `react-hooks` plugin doesn't recognize the ref returned from `useLatest` hook as a Ref.
// Please be cautious about the dependencies for this callback!
// eslint-disable-next-line react-hooks/exhaustive-deps
[addToQueue, buildOptions, consent, setDeferredIdentity, trackingPolicy],
);

function alias(userId: string, previousId: string) {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log(`analytics.alias(${stringify(userId, previousId)})`);
}

addToQueue(() => {
if (window.analytics && window.analytics.alias) {
window.analytics.alias(userId, previousId, buildOptions());
const alias = useCallback(
(userId: string, previousId: string) => {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log(`analytics.alias(${stringify(userId, previousId)})`);
}
});
}

function page(
category?: string,
name?: string,
properties: object = {},
callback?: () => void,
) {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log(`analytics.page(${stringify(category, name, properties)})`);
}
addToQueue(() => {
if (analyticsRef.current && analyticsRef.current.alias) {
analyticsRef.current.alias(userId, previousId, buildOptions());
}
});
},
// The `react-hooks` plugin doesn't recognize the ref returned from `useLatest` hook as a Ref.
// Please be cautious about the dependencies for this callback!
// eslint-disable-next-line react-hooks/exhaustive-deps
[addToQueue, buildOptions],
);

addToQueue(() => {
if (window.analytics && window.analytics.page) {
window.analytics.page(category, name, properties, buildOptions(), callback);
const page = useCallback(
(category?: string, name?: string, properties: object = {}, callback?: () => void) => {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log(`analytics.page(${stringify(category, name, properties)})`);
}
});
}

function track(event: string, properties: object = {}, callback?: () => void) {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log(`analytics.track(${stringify(event, properties)})`);
}
addToQueue(() => {
if (analyticsRef.current && analyticsRef.current.page) {
analyticsRef.current.page(category, name, properties, buildOptions(), callback);
}
});
},
// The `react-hooks` plugin doesn't recognize the ref returned from `useLatest` hook as a Ref.
// Please be cautious about the dependencies for this callback!
// eslint-disable-next-line react-hooks/exhaustive-deps
[addToQueue, buildOptions],
);

addToQueue(() => {
if (window.analytics && window.analytics.track) {
window.analytics.track(event, properties, buildOptions(), callback);
const track = useCallback(
(event: string, properties: object = {}, callback?: () => void) => {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log(`analytics.track(${stringify(event, properties)})`);
}
});
}

function user() {
if (window.analytics && window.analytics.user) {
return window.analytics.user();
addToQueue(() => {
if (analyticsRef.current && analyticsRef.current.track) {
analyticsRef.current.track(event, properties, buildOptions(), callback);
}
});
},
// The `react-hooks` plugin doesn't recognize the ref returned from `useLatest` hook as a Ref.
// Please be cautious about the dependencies for this callback!
// eslint-disable-next-line react-hooks/exhaustive-deps
[addToQueue, buildOptions],
);

const user = useCallback(() => {
if (analytics && analytics.user) {
return analytics.user();
}

// Return fake user API to keep code working even without analytics.js loaded
Expand All @@ -116,16 +134,16 @@ export function useAnalytics() {
return null;
},
};
}
}, [analytics]);

useEffect(() => {
// We are using simple queue to trigger tracking calls
// that might have been created before analytics.js was loaded.
if (isAnalyticsReady && firstInQueue) {
if (analytics && firstInQueue) {
firstInQueue();
removeFromQueue();
}
}, [firstInQueue, isAnalyticsReady, removeFromQueue]);
}, [firstInQueue, analytics, removeFromQueue]);

useEffect(() => {
if (consent) {
Expand All @@ -142,7 +160,7 @@ export function useAnalytics() {

user().id(null); // erase user ID
}
}, [consent, deferredIdentity, identify, removeDeferredIdentity, setDeferredIdentity]);
}, [consent, deferredIdentity, identify, user, removeDeferredIdentity, setDeferredIdentity]);

if (!isEnabled) {
return {
Expand All @@ -154,6 +172,7 @@ export function useAnalytics() {
};
}

// TODO: Expose all methods of analytics-next (might not be needed, since we already provide the `analytics` object)
return {
alias,
identify,
Expand Down
1 change: 0 additions & 1 deletion packages/analytics-nextjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@ export * from './components';
export { AnalyticsContextProvider } from './context';
export * from './events';
export { useAnalytics } from './hooks';
export type { AnalyticsJS } from './types';
Loading