Skip to content

Commit f543e14

Browse files
authored
update advanced analytics (Hashnode#115)
* update umami analytics * update gql schema * refactor and misc fixes
1 parent 0e4ef5f commit f543e14

20 files changed

Lines changed: 2440 additions & 190 deletions

File tree

packages/blog-starter-kit/themes/enterprise/components/analytics.tsx

Lines changed: 88 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,14 @@
11
import Cookies from 'js-cookie';
22
import { useEffect } from 'react';
33
import { v4 as uuidv4 } from 'uuid';
4-
import { useAppContext } from './contexts/appContext';
54

5+
import { useAppContext } from './contexts/appContext';
66
const GA_TRACKING_ID = 'G-72XG3F8LNJ'; // This is Hashnode's GA tracking ID
77
const isProd = process.env.NEXT_PUBLIC_MODE === 'production';
88
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_URL || '';
99

1010
export const Analytics = () => {
11-
const { publication, post } = useAppContext();
12-
13-
useEffect(() => {
14-
if (!isProd) return;
15-
16-
_sendPageViewsToHashnodeGoogleAnalytics();
17-
_sendViewsToHashnodeInternalAnalytics();
18-
_sendViewsToHashnodeAnalyticsDashboard();
19-
}, []);
20-
21-
if (!isProd) return null;
11+
const { publication, post, series, page } = useAppContext();
2212

2313
const _sendPageViewsToHashnodeGoogleAnalytics = () => {
2414
// @ts-ignore
@@ -107,27 +97,6 @@ export const Analytics = () => {
10797
utm_content,
10898
};
10999

110-
// send to Umami powered advanced Hashnode analytics
111-
if (publication.integrations?.umamiWebsiteUUID) {
112-
await fetch(`${BASE_PATH}/api/collect`, {
113-
method: 'POST',
114-
headers: {
115-
'Content-Type': 'application/json',
116-
},
117-
body: JSON.stringify({
118-
payload: {
119-
website: publication.integrations.umamiWebsiteUUID,
120-
url: window.location.pathname,
121-
referrer: referrer,
122-
hostname: window.location.hostname,
123-
language: NAVIGATOR.language,
124-
screen: `${window.screen.width}x${window.screen.height}`,
125-
},
126-
type: 'pageview',
127-
}),
128-
});
129-
}
130-
131100
// For Hashnode Blog Dashboard Analytics
132101
fetch(`${BASE_PATH}/ping/view`, {
133102
method: 'POST',
@@ -138,5 +107,91 @@ export const Analytics = () => {
138107
});
139108
};
140109

110+
function _sendViewsToAdvancedAnalyticsDashboard() {
111+
const publicationId = publication.id;
112+
const postId = post && post.id;
113+
const seriesId = series?.id || post?.series?.id;
114+
const staticPageId = page && page.id;
115+
116+
const data = {
117+
publicationId,
118+
postId,
119+
seriesId,
120+
staticPageId,
121+
};
122+
123+
if (!publicationId) {
124+
console.warn('Publication ID is missing; could not send analytics.');
125+
return;
126+
}
127+
128+
const isBrowser = typeof window !== 'undefined';
129+
if (!isBrowser) {
130+
return;
131+
}
132+
133+
const isLocalhost = window.location.hostname === 'localhost';
134+
if (isLocalhost) {
135+
console.warn(
136+
'Analytics API call is skipped because you are running on localhost; data:',
137+
data,
138+
);
139+
return;
140+
}
141+
142+
const event = {
143+
// timestamp will be added in API
144+
payload: {
145+
publicationId,
146+
postId: postId || null,
147+
seriesId: seriesId || null,
148+
pageId: staticPageId || null,
149+
url: window.location.href,
150+
referrer: document.referrer || null,
151+
language: navigator.language || null,
152+
screen: `${window.screen.width}x${window.screen.height}`,
153+
},
154+
type: 'pageview',
155+
};
156+
157+
const blob = new Blob(
158+
[
159+
JSON.stringify({
160+
events: [event],
161+
}),
162+
],
163+
{
164+
type: 'application/json; charset=UTF-8',
165+
},
166+
);
167+
168+
let hasSentBeacon = false;
169+
try {
170+
if (navigator.sendBeacon) {
171+
hasSentBeacon = navigator.sendBeacon(`${BASE_PATH}/api/analytics`, blob);
172+
}
173+
} catch (error) {
174+
// do nothing; in case there is an error we fall back to fetch
175+
}
176+
177+
if (!hasSentBeacon) {
178+
fetch(`${BASE_PATH}/api/analytics`, {
179+
method: 'POST',
180+
body: blob,
181+
credentials: 'omit',
182+
keepalive: true,
183+
});
184+
}
185+
}
186+
187+
useEffect(() => {
188+
if (!isProd) return;
189+
190+
_sendPageViewsToHashnodeGoogleAnalytics();
191+
_sendViewsToHashnodeInternalAnalytics();
192+
_sendViewsToHashnodeAnalyticsDashboard();
193+
_sendViewsToAdvancedAnalyticsDashboard();
194+
}, []);
195+
141196
return null;
142197
};

packages/blog-starter-kit/themes/enterprise/components/contexts/appContext.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,40 @@
11
import React, { createContext, useContext } from 'react';
2-
import { PostFullFragment, PublicationFragment } from '../../generated/graphql';
2+
import {
3+
PostFullFragment,
4+
PublicationFragment,
5+
SeriesPostsByPublicationQuery,
6+
StaticPageFragment,
7+
} from '../../generated/graphql';
38

4-
type AppContext = { publication: PublicationFragment; post: PostFullFragment | null };
9+
type AppContext = {
10+
publication: PublicationFragment;
11+
post: PostFullFragment | null;
12+
page: StaticPageFragment | null;
13+
series: NonNullable<SeriesPostsByPublicationQuery['publication']>['series'];
14+
};
515

616
const AppContext = createContext<AppContext | null>(null);
717

818
const AppProvider = ({
919
children,
1020
publication,
1121
post,
22+
page,
23+
series,
1224
}: {
1325
children: React.ReactNode;
1426
publication: PublicationFragment;
1527
post?: PostFullFragment | null;
28+
page?: StaticPageFragment | null;
29+
series?: NonNullable<SeriesPostsByPublicationQuery['publication']>['series'];
1630
}) => {
1731
return (
1832
<AppContext.Provider
1933
value={{
2034
publication,
2135
post: post ?? null,
36+
page: page ?? null,
37+
series: series ?? null,
2238
}}
2339
>
2440
{children}

0 commit comments

Comments
 (0)