Skip to content

Commit e973dc2

Browse files
authored
refactor(google-analytics): Switch to V2 + Vite (#502)
1 parent fd37027 commit e973dc2

14 files changed

+328
-60
lines changed

google-analytics/.eslintrc.cjs

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* This is intended to be a basic starting point for linting in your app.
3+
* It relies on recommended configs out of the box for simplicity, but you can
4+
* and should modify this configuration to best suit your team's needs.
5+
*/
6+
7+
/** @type {import('eslint').Linter.Config} */
8+
module.exports = {
9+
root: true,
10+
parserOptions: {
11+
ecmaVersion: "latest",
12+
sourceType: "module",
13+
ecmaFeatures: {
14+
jsx: true,
15+
},
16+
},
17+
env: {
18+
browser: true,
19+
commonjs: true,
20+
es6: true,
21+
},
22+
ignorePatterns: ["!**/.server", "!**/.client"],
23+
24+
// Base config
25+
extends: ["eslint:recommended"],
26+
27+
overrides: [
28+
// React
29+
{
30+
files: ["**/*.{js,jsx,ts,tsx}"],
31+
plugins: ["react", "jsx-a11y"],
32+
extends: [
33+
"plugin:react/recommended",
34+
"plugin:react/jsx-runtime",
35+
"plugin:react-hooks/recommended",
36+
"plugin:jsx-a11y/recommended",
37+
],
38+
settings: {
39+
react: {
40+
version: "detect",
41+
},
42+
formComponents: ["Form"],
43+
linkComponents: [
44+
{ name: "Link", linkAttribute: "to" },
45+
{ name: "NavLink", linkAttribute: "to" },
46+
],
47+
"import/resolver": {
48+
typescript: {},
49+
},
50+
},
51+
},
52+
53+
// Typescript
54+
{
55+
files: ["**/*.{ts,tsx}"],
56+
plugins: ["@typescript-eslint", "import"],
57+
parser: "@typescript-eslint/parser",
58+
settings: {
59+
"import/internal-regex": "^~/",
60+
"import/resolver": {
61+
node: {
62+
extensions: [".ts", ".tsx"],
63+
},
64+
typescript: {
65+
alwaysTryTypes: true,
66+
},
67+
},
68+
},
69+
extends: [
70+
"plugin:@typescript-eslint/recommended",
71+
"plugin:import/recommended",
72+
"plugin:import/typescript",
73+
],
74+
},
75+
76+
// Node
77+
{
78+
files: [".eslintrc.cjs"],
79+
env: {
80+
node: true,
81+
},
82+
},
83+
],
84+
};

google-analytics/.eslintrc.js

-4
This file was deleted.

google-analytics/.gitignore

-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,4 @@ node_modules
22

33
/.cache
44
/build
5-
/public/build
65
.env

google-analytics/README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ Open this example on [CodeSandbox](https://codesandbox.com):
1212

1313
This example shows how to use Google analytics with Remix.
1414

15-
First you have to get the Google analytics ID and add that key in the [.env.example](./.env.example) file.
15+
- Copy `.env.example` to `.env`
16+
- Configure your Google Analytics ID in the `.env` file (read from the `entry.server.tsx` file)
17+
- Run the project
1618

1719
Check [app/root.tsx](./app/root.tsx) where page tracking code is added. For tracking events check [app/routes/contact.tsx](./app/routes/contact.tsx) file.
1820

21+
The Google Analytics tag is disabled in "development", you can enable it during your test, but make sure to only enable tracking in production.
22+
1923
## Related Links
2024

2125
[Google Analytics](https://analytics.google.com/analytics/web/)

google-analytics/app/entry.server.tsx

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { PassThrough } from "node:stream";
2+
3+
import type { AppLoadContext, EntryContext } from "@remix-run/node";
4+
import { createReadableStreamFromReadable } from "@remix-run/node";
5+
import { RemixServer } from "@remix-run/react";
6+
import { config } from "dotenv";
7+
import * as isbotModule from "isbot";
8+
import { renderToPipeableStream } from "react-dom/server";
9+
10+
// Load .env variables
11+
config();
12+
13+
const ABORT_DELAY = 5_000;
14+
15+
export default function handleRequest(
16+
request: Request,
17+
responseStatusCode: number,
18+
responseHeaders: Headers,
19+
remixContext: EntryContext,
20+
loadContext: AppLoadContext,
21+
) {
22+
const prohibitOutOfOrderStreaming =
23+
isBotRequest(request.headers.get("user-agent")) || remixContext.isSpaMode;
24+
25+
return prohibitOutOfOrderStreaming
26+
? handleBotRequest(
27+
request,
28+
responseStatusCode,
29+
responseHeaders,
30+
remixContext,
31+
)
32+
: handleBrowserRequest(
33+
request,
34+
responseStatusCode,
35+
responseHeaders,
36+
remixContext,
37+
);
38+
}
39+
40+
// We have some Remix apps in the wild already running with isbot@3 so we need
41+
// to maintain backwards compatibility even though we want new apps to use
42+
// isbot@4. That way, we can ship this as a minor Semver update to @remix-run/dev.
43+
function isBotRequest(userAgent: string | null) {
44+
if (!userAgent) {
45+
return false;
46+
}
47+
48+
// isbot >= 3.8.0, >4
49+
if ("isbot" in isbotModule && typeof isbotModule.isbot === "function") {
50+
return isbotModule.isbot(userAgent);
51+
}
52+
53+
// isbot < 3.8.0
54+
if ("default" in isbotModule && typeof isbotModule.default === "function") {
55+
return isbotModule.default(userAgent);
56+
}
57+
58+
return false;
59+
}
60+
61+
function handleBotRequest(
62+
request: Request,
63+
responseStatusCode: number,
64+
responseHeaders: Headers,
65+
remixContext: EntryContext,
66+
) {
67+
return new Promise((resolve, reject) => {
68+
let shellRendered = false;
69+
const { pipe, abort } = renderToPipeableStream(
70+
<RemixServer
71+
context={remixContext}
72+
url={request.url}
73+
abortDelay={ABORT_DELAY}
74+
/>,
75+
{
76+
onAllReady() {
77+
shellRendered = true;
78+
const body = new PassThrough();
79+
const stream = createReadableStreamFromReadable(body);
80+
81+
responseHeaders.set("Content-Type", "text/html");
82+
83+
resolve(
84+
new Response(stream, {
85+
headers: responseHeaders,
86+
status: responseStatusCode,
87+
}),
88+
);
89+
90+
pipe(body);
91+
},
92+
onShellError(error: unknown) {
93+
reject(error);
94+
},
95+
onError(error: unknown) {
96+
responseStatusCode = 500;
97+
// Log streaming rendering errors from inside the shell. Don't log
98+
// errors encountered during initial shell rendering since they'll
99+
// reject and get logged in handleDocumentRequest.
100+
if (shellRendered) {
101+
console.error(error);
102+
}
103+
},
104+
},
105+
);
106+
107+
setTimeout(abort, ABORT_DELAY);
108+
});
109+
}
110+
111+
function handleBrowserRequest(
112+
request: Request,
113+
responseStatusCode: number,
114+
responseHeaders: Headers,
115+
remixContext: EntryContext,
116+
) {
117+
return new Promise((resolve, reject) => {
118+
let shellRendered = false;
119+
const { pipe, abort } = renderToPipeableStream(
120+
<RemixServer
121+
context={remixContext}
122+
url={request.url}
123+
abortDelay={ABORT_DELAY}
124+
/>,
125+
{
126+
onShellReady() {
127+
shellRendered = true;
128+
const body = new PassThrough();
129+
const stream = createReadableStreamFromReadable(body);
130+
131+
responseHeaders.set("Content-Type", "text/html");
132+
133+
resolve(
134+
new Response(stream, {
135+
headers: responseHeaders,
136+
status: responseStatusCode,
137+
}),
138+
);
139+
140+
pipe(body);
141+
},
142+
onShellError(error: unknown) {
143+
reject(error);
144+
},
145+
onError(error: unknown) {
146+
responseStatusCode = 500;
147+
// Log streaming rendering errors from inside the shell. Don't log
148+
// errors encountered during initial shell rendering since they'll
149+
// reject and get logged in handleDocumentRequest.
150+
if (shellRendered) {
151+
console.error(error);
152+
}
153+
},
154+
},
155+
);
156+
157+
setTimeout(abort, ABORT_DELAY);
158+
});
159+
}

google-analytics/app/root.tsx

+8-13
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import type { MetaFunction } from "@remix-run/node";
21
import { json } from "@remix-run/node";
3-
// import { json } from "@remix-run/node";
42
import {
53
Link,
64
Links,
7-
LiveReload,
85
Meta,
96
Outlet,
107
Scripts,
@@ -35,13 +32,7 @@ export const loader = async () => {
3532
return json({ gaTrackingId: process.env.GA_TRACKING_ID });
3633
};
3734

38-
export const meta: MetaFunction = () => ({
39-
charset: "utf-8",
40-
title: "New Remix App",
41-
viewport: "width=device-width,initial-scale=1",
42-
});
43-
44-
export default function App() {
35+
export function Layout({ children }: { children: React.ReactNode }) {
4536
const location = useLocation();
4637
const { gaTrackingId } = useLoaderData<typeof loader>();
4738

@@ -54,6 +45,8 @@ export default function App() {
5445
return (
5546
<html lang="en">
5647
<head>
48+
<meta charSet="utf-8" />
49+
<meta name="viewport" content="width=device-width, initial-scale=1" />
5750
<Meta />
5851
<Links />
5952
</head>
@@ -81,7 +74,6 @@ export default function App() {
8174
/>
8275
</>
8376
)}
84-
8577
<header>
8678
<nav>
8779
<ul>
@@ -100,11 +92,14 @@ export default function App() {
10092
</ul>
10193
</nav>
10294
</header>
103-
<Outlet />
95+
{children}
10496
<ScrollRestoration />
10597
<Scripts />
106-
<LiveReload />
10798
</body>
10899
</html>
109100
);
110101
}
102+
103+
export default function App() {
104+
return <Outlet />;
105+
}

google-analytics/app/routes/_index.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { MetaFunction } from "@remix-run/node";
22

3-
export const meta: MetaFunction = () => ({
4-
title: "Home",
5-
});
3+
export const meta: MetaFunction = () => [
4+
{
5+
title: "Home",
6+
},
7+
];
68

79
export default function Index() {
810
return (

google-analytics/app/routes/dashboard.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { MetaFunction } from "@remix-run/node";
22

3-
export const meta: MetaFunction = () => ({
4-
title: "Dashboard",
5-
});
3+
export const meta: MetaFunction = () => [
4+
{
5+
title: "Dashboard",
6+
},
7+
];
68

79
export default function Dashboard() {
810
return (

google-analytics/app/routes/profile.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { MetaFunction } from "@remix-run/node";
22

3-
export const meta: MetaFunction = () => ({
4-
title: "Profile",
5-
});
3+
export const meta: MetaFunction = () => [
4+
{
5+
title: "Profile",
6+
},
7+
];
68

79
export default function Profile() {
810
return (

0 commit comments

Comments
 (0)