@launch77-shared/plugin-analytics-web 0.0.1
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/README.md +39 -0
- package/dist/generator.js +300 -0
- package/package.json +46 -0
- package/plugin.json +7 -0
- package/templates/.env.example +24 -0
- package/templates/src/.gitkeep +0 -0
- package/templates/src/app/analytics-test/components/CodeExample.tsx +14 -0
- package/templates/src/app/analytics-test/components/ConsentDisplay.tsx +61 -0
- package/templates/src/app/analytics-test/page.tsx +200 -0
- package/templates/src/modules/analytics/README.md +550 -0
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# AnalyticsWeb Plugin
|
|
2
|
+
|
|
3
|
+
Launch77 plugin for analytics-web
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
launch77 plugin:install analytics-web
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
After installation, the plugin will:
|
|
14
|
+
|
|
15
|
+
- TODO: Describe what the plugin does
|
|
16
|
+
- TODO: List any files created or modified
|
|
17
|
+
- TODO: Explain configuration options
|
|
18
|
+
|
|
19
|
+
## Development
|
|
20
|
+
|
|
21
|
+
### Building
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm run build
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Testing
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm run typecheck
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Template Files
|
|
34
|
+
|
|
35
|
+
The `templates/` directory contains files that will be copied to the target application when this plugin is installed. Add any template files your plugin needs here.
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
UNLICENSED
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/generator.ts
|
|
4
|
+
import * as path2 from "path";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { StandardGenerator } from "@launch77/plugin-runtime";
|
|
7
|
+
|
|
8
|
+
// src/utils/layout-modifier.ts
|
|
9
|
+
import fs from "fs/promises";
|
|
10
|
+
async function wrapWithAnalyticsProvider(layoutPath) {
|
|
11
|
+
try {
|
|
12
|
+
const fileExists = await fs.access(layoutPath).then(() => true).catch(() => false);
|
|
13
|
+
if (!fileExists) {
|
|
14
|
+
return {
|
|
15
|
+
success: false,
|
|
16
|
+
error: `File not found: ${layoutPath}`
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
let content = await fs.readFile(layoutPath, "utf-8");
|
|
20
|
+
if (content.includes("AnalyticsProvider") || content.includes("@launch77-shared/lib-analytics-web")) {
|
|
21
|
+
return {
|
|
22
|
+
success: true,
|
|
23
|
+
alreadyExists: true
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const importStatement = `import { AnalyticsProvider, CookieConsent } from '@launch77-shared/lib-analytics-web'
|
|
27
|
+
`;
|
|
28
|
+
const lines = content.split("\n");
|
|
29
|
+
let lastImportIndex = -1;
|
|
30
|
+
for (let i = 0; i < lines.length; i++) {
|
|
31
|
+
if (lines[i].trim().startsWith("import ") || lines[i].trim().startsWith("import{")) {
|
|
32
|
+
lastImportIndex = i;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (lastImportIndex !== -1) {
|
|
36
|
+
lines.splice(lastImportIndex + 1, 0, importStatement);
|
|
37
|
+
} else {
|
|
38
|
+
let insertIndex = 0;
|
|
39
|
+
for (let i = 0; i < lines.length; i++) {
|
|
40
|
+
if (lines[i].includes("'use client'") || lines[i].includes("'use server'") || lines[i].includes('"use client"') || lines[i].includes('"use server"')) {
|
|
41
|
+
insertIndex = i + 1;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
lines.splice(insertIndex, 0, "", importStatement);
|
|
46
|
+
}
|
|
47
|
+
content = lines.join("\n");
|
|
48
|
+
const childrenRegex = /(<html[^>]*>[\s\S]*?)(\{children\})([\s\S]*?<\/html>)/;
|
|
49
|
+
const match = content.match(childrenRegex);
|
|
50
|
+
if (!match) {
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
error: "Could not find {children} in layout.tsx"
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const _beforeChildren = match[1];
|
|
57
|
+
const childrenPlaceholder = match[2];
|
|
58
|
+
const _afterChildren = match[3];
|
|
59
|
+
const childrenLine = content.split("\n").find((line) => line.includes("{children}"));
|
|
60
|
+
const indentMatch = childrenLine?.match(/^(\s+)/);
|
|
61
|
+
const baseIndent = indentMatch ? indentMatch[1] : " ";
|
|
62
|
+
const wrappedChildren = `<AnalyticsProvider
|
|
63
|
+
${baseIndent} gtmId={process.env.NEXT_PUBLIC_GTM_ID}
|
|
64
|
+
${baseIndent} posthogKey={process.env.NEXT_PUBLIC_POSTHOG_KEY}
|
|
65
|
+
${baseIndent}>
|
|
66
|
+
${baseIndent} ${childrenPlaceholder}
|
|
67
|
+
${baseIndent} <CookieConsent />
|
|
68
|
+
${baseIndent}</AnalyticsProvider>`;
|
|
69
|
+
content = content.replace(childrenRegex, `$1${wrappedChildren}$3`);
|
|
70
|
+
await fs.writeFile(layoutPath, content, "utf-8");
|
|
71
|
+
return {
|
|
72
|
+
success: true
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
error: error instanceof Error ? error.message : String(error)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/utils/env-modifier.ts
|
|
83
|
+
import fs2 from "fs/promises";
|
|
84
|
+
import path from "path";
|
|
85
|
+
async function ensureEnvVariables(appPath) {
|
|
86
|
+
try {
|
|
87
|
+
const envLocalPath = path.join(appPath, ".env.local");
|
|
88
|
+
const envExamplePath = path.join(appPath, ".env.example");
|
|
89
|
+
const envLocalExists = await fs2.access(envLocalPath).then(() => true).catch(() => false);
|
|
90
|
+
const envExampleExists = await fs2.access(envExamplePath).then(() => true).catch(() => false);
|
|
91
|
+
if (!envLocalExists && !envExampleExists) {
|
|
92
|
+
return {
|
|
93
|
+
success: true,
|
|
94
|
+
created: false
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (!envLocalExists && envExampleExists) {
|
|
98
|
+
const envExampleContent = await fs2.readFile(envExamplePath, "utf-8");
|
|
99
|
+
await fs2.writeFile(envLocalPath, envExampleContent, "utf-8");
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
created: true
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const envContent = await fs2.readFile(envLocalPath, "utf-8");
|
|
106
|
+
const hasGtmId = envContent.includes("NEXT_PUBLIC_GTM_ID");
|
|
107
|
+
const hasPosthogKey = envContent.includes("NEXT_PUBLIC_POSTHOG_KEY");
|
|
108
|
+
if (hasGtmId && hasPosthogKey) {
|
|
109
|
+
return {
|
|
110
|
+
success: true
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
let updatedContent = envContent;
|
|
114
|
+
if (!envContent.endsWith("\n")) {
|
|
115
|
+
updatedContent += "\n";
|
|
116
|
+
}
|
|
117
|
+
updatedContent += "\n# Analytics Configuration (added by analytics-web plugin)\n";
|
|
118
|
+
if (!hasGtmId) {
|
|
119
|
+
updatedContent += "# Get this from https://tagmanager.google.com\nNEXT_PUBLIC_GTM_ID=\n";
|
|
120
|
+
}
|
|
121
|
+
if (!hasPosthogKey) {
|
|
122
|
+
updatedContent += "# Get this from https://posthog.com (Project Settings > API Keys)\nNEXT_PUBLIC_POSTHOG_KEY=\n";
|
|
123
|
+
}
|
|
124
|
+
await fs2.writeFile(envLocalPath, updatedContent, "utf-8");
|
|
125
|
+
return {
|
|
126
|
+
success: true
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return {
|
|
130
|
+
success: false,
|
|
131
|
+
error: error instanceof Error ? error.message : String(error)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/utils/config-modifier.ts
|
|
137
|
+
import fs3 from "fs/promises";
|
|
138
|
+
async function addTailwindContent(filePath, contentPath) {
|
|
139
|
+
try {
|
|
140
|
+
const fileExists = await fs3.access(filePath).then(() => true).catch(() => false);
|
|
141
|
+
if (!fileExists) {
|
|
142
|
+
return {
|
|
143
|
+
success: false,
|
|
144
|
+
error: `File not found: ${filePath}`
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
148
|
+
if (content.includes(contentPath)) {
|
|
149
|
+
return {
|
|
150
|
+
success: true,
|
|
151
|
+
alreadyExists: true
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const lines = content.split("\n");
|
|
155
|
+
let contentArrayStartIndex = -1;
|
|
156
|
+
let contentArrayEndIndex = -1;
|
|
157
|
+
for (let i = 0; i < lines.length; i++) {
|
|
158
|
+
const line = lines[i];
|
|
159
|
+
if (line.includes("content:") && line.includes("[")) {
|
|
160
|
+
contentArrayStartIndex = i;
|
|
161
|
+
if (line.includes("]")) {
|
|
162
|
+
contentArrayEndIndex = i;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
166
|
+
if (lines[j].includes("]")) {
|
|
167
|
+
contentArrayEndIndex = j;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (contentArrayStartIndex === -1) {
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: "Could not find content array in Tailwind config"
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
let indentation = " ";
|
|
181
|
+
for (let i = contentArrayStartIndex + 1; i < contentArrayEndIndex; i++) {
|
|
182
|
+
const match = lines[i].match(/^(\s+)['"]/);
|
|
183
|
+
if (match) {
|
|
184
|
+
indentation = match[1];
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const newLine = `${indentation}'${contentPath}',`;
|
|
189
|
+
lines.splice(contentArrayEndIndex, 0, newLine);
|
|
190
|
+
const newContent = lines.join("\n");
|
|
191
|
+
await fs3.writeFile(filePath, newContent, "utf-8");
|
|
192
|
+
return {
|
|
193
|
+
success: true
|
|
194
|
+
};
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return {
|
|
197
|
+
success: false,
|
|
198
|
+
error: error instanceof Error ? error.message : String(error)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/generator.ts
|
|
204
|
+
var AnalyticsWebGenerator = class extends StandardGenerator {
|
|
205
|
+
constructor(context) {
|
|
206
|
+
super(context);
|
|
207
|
+
}
|
|
208
|
+
async injectCode() {
|
|
209
|
+
console.log(chalk.cyan("\u{1F527} Configuring analytics...\n"));
|
|
210
|
+
await this.updateTailwindConfig();
|
|
211
|
+
await this.wrapLayout();
|
|
212
|
+
await this.updateEnvFile();
|
|
213
|
+
}
|
|
214
|
+
async updateTailwindConfig() {
|
|
215
|
+
const tailwindConfigPath = path2.join(this.context.appPath, "tailwind.config.ts");
|
|
216
|
+
const result1 = await addTailwindContent(tailwindConfigPath, "node_modules/@launch77-shared/lib-analytics-web/dist/**/*.js");
|
|
217
|
+
const result2 = await addTailwindContent(tailwindConfigPath, "../../node_modules/@launch77-shared/lib-analytics-web/dist/**/*.js");
|
|
218
|
+
if (result1.success || result2.success) {
|
|
219
|
+
if (result1.alreadyExists && result2.alreadyExists) {
|
|
220
|
+
console.log(chalk.gray(" \u2713 Analytics library already configured in tailwind.config.ts"));
|
|
221
|
+
} else {
|
|
222
|
+
console.log(chalk.green(" \u2713 Added Analytics library to Tailwind content paths"));
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
console.log(chalk.yellow(` \u26A0\uFE0F Could not auto-configure tailwind.config.ts: ${result1.error}`));
|
|
226
|
+
console.log(chalk.gray(" You will need to add the content paths manually:"));
|
|
227
|
+
console.log(chalk.gray(" 'node_modules/@launch77-shared/lib-analytics-web/dist/**/*.js'"));
|
|
228
|
+
console.log(chalk.gray(" '../../node_modules/@launch77-shared/lib-analytics-web/dist/**/*.js'"));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async wrapLayout() {
|
|
232
|
+
const layoutPath = path2.join(this.context.appPath, "src/app/layout.tsx");
|
|
233
|
+
const result = await wrapWithAnalyticsProvider(layoutPath);
|
|
234
|
+
if (result.success) {
|
|
235
|
+
if (result.alreadyExists) {
|
|
236
|
+
console.log(chalk.gray(" \u2713 AnalyticsProvider already configured in layout.tsx"));
|
|
237
|
+
} else {
|
|
238
|
+
console.log(chalk.green(" \u2713 Wrapped app with AnalyticsProvider in layout.tsx"));
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
console.log(chalk.yellow(` \u26A0\uFE0F Could not auto-configure layout.tsx: ${result.error}`));
|
|
242
|
+
console.log(chalk.gray(" You will need to add AnalyticsProvider manually"));
|
|
243
|
+
console.log(chalk.gray(" See src/modules/analytics/README.md for instructions"));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async updateEnvFile() {
|
|
247
|
+
const result = await ensureEnvVariables(this.context.appPath);
|
|
248
|
+
if (result.success) {
|
|
249
|
+
if (result.created) {
|
|
250
|
+
console.log(chalk.green(" \u2713 Created .env.local from .env.example"));
|
|
251
|
+
} else {
|
|
252
|
+
console.log(chalk.gray(" \u2713 Environment variables template ready"));
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
console.log(chalk.yellow(` \u26A0\uFE0F Could not configure environment: ${result.error}`));
|
|
256
|
+
console.log(chalk.gray(" See .env.example for required variables"));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
showNextSteps() {
|
|
260
|
+
console.log(chalk.bold.green("\n\u2705 Analytics Plugin Installed!\n"));
|
|
261
|
+
console.log(chalk.bold("Next Steps:\n"));
|
|
262
|
+
console.log("1. Add your analytics credentials to .env.local:");
|
|
263
|
+
console.log(chalk.cyan(" NEXT_PUBLIC_GTM_ID=GTM-XXXXXXX"));
|
|
264
|
+
console.log(chalk.cyan(" NEXT_PUBLIC_POSTHOG_KEY=phc_xxxxx...\n"));
|
|
265
|
+
console.log("2. Test analytics tracking:");
|
|
266
|
+
console.log(chalk.cyan(" Visit http://localhost:3000/analytics-test\n"));
|
|
267
|
+
console.log("3. Customize the consent banner:");
|
|
268
|
+
console.log(chalk.cyan(" Edit src/components/ConsentBanner.tsx\n"));
|
|
269
|
+
console.log(chalk.white("Documentation:\n"));
|
|
270
|
+
console.log(chalk.gray("See src/modules/analytics/README.md for:\n"));
|
|
271
|
+
console.log(chalk.gray(" \u2022 How to get GTM ID and PostHog key"));
|
|
272
|
+
console.log(chalk.gray(" \u2022 Usage examples and best practices"));
|
|
273
|
+
console.log(chalk.gray(" \u2022 Privacy and GDPR compliance"));
|
|
274
|
+
console.log(chalk.gray(" \u2022 Debugging and troubleshooting\n"));
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
async function main() {
|
|
278
|
+
const args = process.argv.slice(2);
|
|
279
|
+
const appPath = args.find((arg) => arg.startsWith("--appPath="))?.split("=")[1];
|
|
280
|
+
const appName = args.find((arg) => arg.startsWith("--appName="))?.split("=")[1];
|
|
281
|
+
const workspaceName = args.find((arg) => arg.startsWith("--workspaceName="))?.split("=")[1];
|
|
282
|
+
const pluginPath = args.find((arg) => arg.startsWith("--pluginPath="))?.split("=")[1];
|
|
283
|
+
if (!appPath || !appName || !workspaceName || !pluginPath) {
|
|
284
|
+
console.error(chalk.red("Error: Missing required arguments"));
|
|
285
|
+
console.error(chalk.gray("Usage: --appPath=<path> --appName=<name> --workspaceName=<name> --pluginPath=<path>"));
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
const generator = new AnalyticsWebGenerator({ appPath, appName, workspaceName, pluginPath });
|
|
289
|
+
await generator.run();
|
|
290
|
+
}
|
|
291
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
292
|
+
main().catch((error) => {
|
|
293
|
+
console.error(chalk.red("\n\u274C Error during plugin setup:"));
|
|
294
|
+
console.error(error);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
export {
|
|
299
|
+
AnalyticsWebGenerator
|
|
300
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@launch77-shared/plugin-analytics-web",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Launch77 plugin for analytics-web",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/generator.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"generate": "./dist/generator.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/",
|
|
13
|
+
"templates/",
|
|
14
|
+
"plugin.json"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"dev": "tsup --watch",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"lint": "eslint src/**/*.ts",
|
|
21
|
+
"release:connect": "launch77-release-connect",
|
|
22
|
+
"release:verify": "launch77-release-verify"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@launch77/plugin-runtime": "^0.1.0",
|
|
26
|
+
"chalk": "^5.3.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^20.10.0",
|
|
30
|
+
"tsup": "^8.0.0",
|
|
31
|
+
"typescript": "^5.3.0"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"launch77": {
|
|
37
|
+
"installedPlugins": {
|
|
38
|
+
"release": {
|
|
39
|
+
"package": "release",
|
|
40
|
+
"version": "1.0.1",
|
|
41
|
+
"installedAt": "2026-01-26T01:02:24.730Z",
|
|
42
|
+
"source": "local"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
package/plugin.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Analytics Configuration
|
|
2
|
+
# Copy this file to .env.local and fill in your actual values
|
|
3
|
+
|
|
4
|
+
# Google Tag Manager ID
|
|
5
|
+
# Get this from: https://tagmanager.google.com
|
|
6
|
+
# Format: GTM-XXXXXXX
|
|
7
|
+
NEXT_PUBLIC_GTM_ID=
|
|
8
|
+
|
|
9
|
+
# PostHog API Key
|
|
10
|
+
# Get this from: https://posthog.com (Project Settings > API Keys)
|
|
11
|
+
# Format: phc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
12
|
+
NEXT_PUBLIC_POSTHOG_KEY=
|
|
13
|
+
|
|
14
|
+
# PostHog Host (optional - defaults to https://app.posthog.com)
|
|
15
|
+
# Use this if you're self-hosting PostHog
|
|
16
|
+
# NEXT_PUBLIC_POSTHOG_HOST=https://your-posthog-instance.com
|
|
17
|
+
|
|
18
|
+
# Analytics Configuration
|
|
19
|
+
# Enable/disable analytics entirely (useful for development)
|
|
20
|
+
# NEXT_PUBLIC_ANALYTICS_ENABLED=true
|
|
21
|
+
|
|
22
|
+
# Development Mode
|
|
23
|
+
# In development, analytics events are logged to console but may not be sent
|
|
24
|
+
# to production endpoints (depends on your GTM/PostHog configuration)
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface CodeExampleProps {
|
|
2
|
+
children: string
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Component for displaying formatted code examples in analytics test page
|
|
7
|
+
*/
|
|
8
|
+
export function CodeExample({ children }: CodeExampleProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="mt-4 rounded bg-muted p-3">
|
|
11
|
+
<code className="text-xs whitespace-pre-wrap">{children}</code>
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ConsentState } from '@launch77-shared/lib-analytics-web'
|
|
2
|
+
|
|
3
|
+
interface ConsentDisplayProps {
|
|
4
|
+
consent: ConsentState
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Type-safe component for displaying consent state
|
|
9
|
+
* Handles all three states: undefined (loading), 'NOT_SET', and ConsentPreferences object
|
|
10
|
+
*/
|
|
11
|
+
export function ConsentDisplay({ consent }: ConsentDisplayProps) {
|
|
12
|
+
// Loading state
|
|
13
|
+
if (consent === undefined) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="space-y-2">
|
|
16
|
+
<p>
|
|
17
|
+
<strong>Current Consent:</strong> <span className="rounded px-2 py-1 text-sm font-medium bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200">Loading...</span>
|
|
18
|
+
</p>
|
|
19
|
+
<p className="text-sm text-muted-foreground">⏳ Initializing analytics system</p>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Not set state
|
|
25
|
+
if (consent === 'NOT_SET') {
|
|
26
|
+
return (
|
|
27
|
+
<div className="space-y-2">
|
|
28
|
+
<p>
|
|
29
|
+
<strong>Current Consent:</strong> <span className="rounded px-2 py-1 text-sm font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">Not Set</span>
|
|
30
|
+
</p>
|
|
31
|
+
<p className="text-sm text-muted-foreground">⚠ Waiting for user consent</p>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ConsentPreferences object - user has made a choice
|
|
37
|
+
return (
|
|
38
|
+
<div className="space-y-3">
|
|
39
|
+
<p>
|
|
40
|
+
<strong>Current Consent:</strong> <span className="rounded px-2 py-1 text-sm font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">Set</span>
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
<div className="space-y-2 text-sm">
|
|
44
|
+
<div className="flex items-center justify-between">
|
|
45
|
+
<span className="text-muted-foreground">Analytics Tracking:</span>
|
|
46
|
+
<span className={`rounded px-2 py-0.5 text-xs font-medium ${consent.analytics ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'}`}>{consent.analytics ? '✓ Enabled' : '✗ Disabled'}</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="flex items-center justify-between">
|
|
49
|
+
<span className="text-muted-foreground">Marketing Tracking:</span>
|
|
50
|
+
<span className={`rounded px-2 py-0.5 text-xs font-medium ${consent.marketing ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'}`}>{consent.marketing ? '✓ Enabled' : '✗ Disabled'}</span>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="flex items-center justify-between">
|
|
53
|
+
<span className="text-muted-foreground">Consent Given:</span>
|
|
54
|
+
<span className="text-xs text-muted-foreground">{new Date(consent.timestamp).toLocaleString()}</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<p className="text-sm text-muted-foreground">{consent.analytics ? '✓ Analytics is tracking events' : '✗ Analytics is disabled (user declined)'}</p>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useAnalytics, usePageTracking, CTAButton } from '@launch77-shared/lib-analytics-web'
|
|
4
|
+
import { useEffect } from 'react'
|
|
5
|
+
import { ConsentDisplay } from './components/ConsentDisplay'
|
|
6
|
+
import { CodeExample } from './components/CodeExample'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Analytics Test Page
|
|
10
|
+
*
|
|
11
|
+
* This page demonstrates all analytics features and helps you verify
|
|
12
|
+
* that tracking is working correctly.
|
|
13
|
+
*
|
|
14
|
+
* Open your browser's Network tab and PostHog/GTM debug tools to see events.
|
|
15
|
+
*/
|
|
16
|
+
export default function AnalyticsTestPage() {
|
|
17
|
+
const { trackClick, trackForm, trackCustom, consent } = useAnalytics()
|
|
18
|
+
|
|
19
|
+
// Auto-track page views on route changes
|
|
20
|
+
usePageTracking()
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
// Track custom event on page load
|
|
24
|
+
trackCustom({
|
|
25
|
+
action: 'page_visit',
|
|
26
|
+
category: 'test',
|
|
27
|
+
label: 'Analytics Test Page',
|
|
28
|
+
metadata: {
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
}, [trackCustom])
|
|
33
|
+
|
|
34
|
+
const handleManualClick = () => {
|
|
35
|
+
trackClick({
|
|
36
|
+
action: 'click_manual_button',
|
|
37
|
+
category: 'test',
|
|
38
|
+
label: 'Manual Tracking Button',
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const handleFormSubmit = (e: React.FormEvent) => {
|
|
43
|
+
e.preventDefault()
|
|
44
|
+
trackForm({
|
|
45
|
+
action: 'submit',
|
|
46
|
+
category: 'test',
|
|
47
|
+
label: 'Test Form',
|
|
48
|
+
formName: 'analytics_test_form',
|
|
49
|
+
})
|
|
50
|
+
alert('Form submitted! Check your analytics dashboard.')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="mx-auto max-w-4xl p-8">
|
|
55
|
+
<h1 className="mb-8 text-4xl font-bold">Analytics Test Page</h1>
|
|
56
|
+
|
|
57
|
+
{/* Consent Status */}
|
|
58
|
+
<section className="mb-8 rounded-lg border border-border bg-card p-6">
|
|
59
|
+
<h2 className="mb-4 text-2xl font-semibold">Consent Status</h2>
|
|
60
|
+
<ConsentDisplay consent={consent} />
|
|
61
|
+
</section>
|
|
62
|
+
|
|
63
|
+
{/* Manual Tracking */}
|
|
64
|
+
<section className="mb-8 rounded-lg border border-border bg-card p-6">
|
|
65
|
+
<h2 className="mb-4 text-2xl font-semibold">Manual Event Tracking</h2>
|
|
66
|
+
<p className="mb-4 text-muted-foreground">
|
|
67
|
+
Use the <code className="rounded bg-muted px-1 py-0.5">trackClick()</code> hook to manually track events.
|
|
68
|
+
</p>
|
|
69
|
+
<button onClick={handleManualClick} className="rounded-lg bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90">
|
|
70
|
+
Track Manual Click
|
|
71
|
+
</button>
|
|
72
|
+
<CodeExample>{`trackClick({
|
|
73
|
+
action: 'click_manual_button',
|
|
74
|
+
category: 'test',
|
|
75
|
+
label: 'Manual Tracking Button'
|
|
76
|
+
})`}</CodeExample>
|
|
77
|
+
</section>
|
|
78
|
+
|
|
79
|
+
{/* CTAButton Component */}
|
|
80
|
+
<section className="mb-8 rounded-lg border border-border bg-card p-6">
|
|
81
|
+
<h2 className="mb-4 text-2xl font-semibold">CTAButton Component</h2>
|
|
82
|
+
<p className="mb-4 text-muted-foreground">
|
|
83
|
+
Use <code className="rounded bg-muted px-1 py-0.5">CTAButton</code> for marketing call-to-action buttons with automatic tracking.
|
|
84
|
+
</p>
|
|
85
|
+
<div className="flex gap-4">
|
|
86
|
+
<CTAButton href="/signup" trackingData={{ location: 'test_page', text: 'Get Started' }}>
|
|
87
|
+
Get Started
|
|
88
|
+
</CTAButton>
|
|
89
|
+
<CTAButton href="/demo" variant="outline" trackingData={{ location: 'test_page', text: 'Book Demo' }}>
|
|
90
|
+
Book Demo
|
|
91
|
+
</CTAButton>
|
|
92
|
+
</div>
|
|
93
|
+
<CodeExample>{`<CTAButton
|
|
94
|
+
href="/signup"
|
|
95
|
+
trackingData={{ location: "test_page", text: "Get Started" }}
|
|
96
|
+
>
|
|
97
|
+
Get Started
|
|
98
|
+
</CTAButton>`}</CodeExample>
|
|
99
|
+
</section>
|
|
100
|
+
|
|
101
|
+
{/* Form Tracking */}
|
|
102
|
+
<section className="mb-8 rounded-lg border border-border bg-card p-6">
|
|
103
|
+
<h2 className="mb-4 text-2xl font-semibold">Form Tracking</h2>
|
|
104
|
+
<p className="mb-4 text-muted-foreground">
|
|
105
|
+
Use <code className="rounded bg-muted px-1 py-0.5">trackForm()</code> to track form interactions.
|
|
106
|
+
</p>
|
|
107
|
+
<form onSubmit={handleFormSubmit} className="space-y-4">
|
|
108
|
+
<div>
|
|
109
|
+
<label htmlFor="name" className="mb-1 block text-sm font-medium">
|
|
110
|
+
Name
|
|
111
|
+
</label>
|
|
112
|
+
<input id="name" type="text" className="w-full rounded-md border border-input bg-background px-3 py-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" placeholder="Enter your name" />
|
|
113
|
+
</div>
|
|
114
|
+
<div>
|
|
115
|
+
<label htmlFor="email" className="mb-1 block text-sm font-medium">
|
|
116
|
+
Email
|
|
117
|
+
</label>
|
|
118
|
+
<input id="email" type="email" className="w-full rounded-md border border-input bg-background px-3 py-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" placeholder="Enter your email" />
|
|
119
|
+
</div>
|
|
120
|
+
<button type="submit" className="rounded-lg bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90">
|
|
121
|
+
Submit Test Form
|
|
122
|
+
</button>
|
|
123
|
+
</form>
|
|
124
|
+
<CodeExample>{`trackForm({
|
|
125
|
+
action: 'submit',
|
|
126
|
+
category: 'test',
|
|
127
|
+
label: 'Test Form',
|
|
128
|
+
formName: 'analytics_test_form'
|
|
129
|
+
})`}</CodeExample>
|
|
130
|
+
</section>
|
|
131
|
+
|
|
132
|
+
{/* Custom Events */}
|
|
133
|
+
<section className="mb-8 rounded-lg border border-border bg-card p-6">
|
|
134
|
+
<h2 className="mb-4 text-2xl font-semibold">Custom Events</h2>
|
|
135
|
+
<p className="mb-4 text-muted-foreground">
|
|
136
|
+
Use <code className="rounded bg-muted px-1 py-0.5">trackCustom()</code> to track any custom event with metadata.
|
|
137
|
+
</p>
|
|
138
|
+
<button
|
|
139
|
+
onClick={() =>
|
|
140
|
+
trackCustom({
|
|
141
|
+
action: 'custom_event',
|
|
142
|
+
category: 'test',
|
|
143
|
+
label: 'Custom Event Example',
|
|
144
|
+
metadata: {
|
|
145
|
+
timestamp: new Date().toISOString(),
|
|
146
|
+
userAction: 'button_click',
|
|
147
|
+
testId: Math.random().toString(36).substring(7),
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
className="rounded-lg bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
|
|
152
|
+
>
|
|
153
|
+
Send Custom Event
|
|
154
|
+
</button>
|
|
155
|
+
<CodeExample>{`trackCustom({
|
|
156
|
+
action: 'custom_event',
|
|
157
|
+
category: 'test',
|
|
158
|
+
label: 'Custom Event Example',
|
|
159
|
+
metadata: {
|
|
160
|
+
timestamp: new Date().toISOString(),
|
|
161
|
+
userAction: 'button_click'
|
|
162
|
+
}
|
|
163
|
+
})`}</CodeExample>
|
|
164
|
+
</section>
|
|
165
|
+
|
|
166
|
+
{/* Debug Instructions */}
|
|
167
|
+
<section className="rounded-lg border border-border bg-muted p-6">
|
|
168
|
+
<h2 className="mb-4 text-2xl font-semibold">Debugging</h2>
|
|
169
|
+
<div className="space-y-4 text-sm">
|
|
170
|
+
<div>
|
|
171
|
+
<h3 className="mb-2 font-semibold">Browser Console</h3>
|
|
172
|
+
<p className="text-muted-foreground">Open DevTools Console to see analytics events logged by the library (in development mode).</p>
|
|
173
|
+
</div>
|
|
174
|
+
<div>
|
|
175
|
+
<h3 className="mb-2 font-semibold">PostHog Debug</h3>
|
|
176
|
+
<p className="text-muted-foreground">
|
|
177
|
+
Install the PostHog toolbar:{' '}
|
|
178
|
+
<a href="https://posthog.com/docs/libraries/js#toolbar" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
|
179
|
+
https://posthog.com/docs/libraries/js#toolbar
|
|
180
|
+
</a>
|
|
181
|
+
</p>
|
|
182
|
+
</div>
|
|
183
|
+
<div>
|
|
184
|
+
<h3 className="mb-2 font-semibold">Google Tag Manager</h3>
|
|
185
|
+
<p className="text-muted-foreground">
|
|
186
|
+
Use GTM Preview mode:{' '}
|
|
187
|
+
<a href="https://tagmanager.google.com/" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
|
188
|
+
https://tagmanager.google.com/
|
|
189
|
+
</a>
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
<div>
|
|
193
|
+
<h3 className="mb-2 font-semibold">Network Tab</h3>
|
|
194
|
+
<p className="text-muted-foreground">Filter by "collect" or "event" to see tracking requests in the Network tab.</p>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</section>
|
|
198
|
+
</div>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
# Launch77 Analytics
|
|
2
|
+
|
|
3
|
+
Privacy-first analytics integration for web applications with GDPR-compliant consent management.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔒 **GDPR Compliant** - Built-in consent management and PII sanitization
|
|
8
|
+
- 📊 **Dual Platform** - Google Tag Manager + PostHog integration
|
|
9
|
+
- 🎯 **Event Tracking** - Page views, clicks, forms, and custom events
|
|
10
|
+
- 🚀 **Ready Components** - Pre-built trackable buttons and links
|
|
11
|
+
- 🔐 **Privacy First** - No tracking until user consent
|
|
12
|
+
- 📱 **Auto Tracking** - SPA page tracking and scroll depth
|
|
13
|
+
- 🎨 **Customizable** - Full control over consent UI and tracking
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
The plugin has already configured your app with:
|
|
18
|
+
|
|
19
|
+
1. ✅ AnalyticsProvider wrapper in your app layout
|
|
20
|
+
2. ✅ Consent banner component
|
|
21
|
+
3. ✅ Environment variables template
|
|
22
|
+
4. ✅ Test page with examples
|
|
23
|
+
|
|
24
|
+
You're ready to configure your analytics platforms!
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
### 1. Set Up Environment Variables
|
|
29
|
+
|
|
30
|
+
Copy `.env.example` to `.env.local` and add your credentials:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# .env.local
|
|
34
|
+
NEXT_PUBLIC_GTM_ID=GTM-XXXXXXX
|
|
35
|
+
NEXT_PUBLIC_POSTHOG_KEY=phc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
#### Getting Your Credentials
|
|
39
|
+
|
|
40
|
+
**Google Tag Manager:**
|
|
41
|
+
|
|
42
|
+
1. Go to [https://tagmanager.google.com](https://tagmanager.google.com)
|
|
43
|
+
2. Create a container (or use existing)
|
|
44
|
+
3. Find your GTM ID in the format `GTM-XXXXXXX`
|
|
45
|
+
|
|
46
|
+
**PostHog:**
|
|
47
|
+
|
|
48
|
+
1. Go to [https://posthog.com](https://posthog.com) (or your self-hosted instance)
|
|
49
|
+
2. Navigate to Project Settings > API Keys
|
|
50
|
+
3. Copy your Project API Key (format: `phc_xxxxx...`)
|
|
51
|
+
|
|
52
|
+
### 2. Verify Setup
|
|
53
|
+
|
|
54
|
+
Visit the test page to verify analytics is working:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm run dev
|
|
58
|
+
# Navigate to http://localhost:3000/analytics-test
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The test page shows:
|
|
62
|
+
|
|
63
|
+
- Consent status
|
|
64
|
+
- Manual event tracking
|
|
65
|
+
- Trackable components
|
|
66
|
+
- Form tracking
|
|
67
|
+
- Custom events
|
|
68
|
+
- Debug instructions
|
|
69
|
+
|
|
70
|
+
### 3. Customize Consent Banner
|
|
71
|
+
|
|
72
|
+
Edit `src/components/ConsentBanner.tsx` to match your brand:
|
|
73
|
+
|
|
74
|
+
- Update text and styling
|
|
75
|
+
- Link to your privacy policy
|
|
76
|
+
- Adjust button design
|
|
77
|
+
- Add additional consent options if needed
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
### Basic Tracking
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
'use client'
|
|
85
|
+
|
|
86
|
+
import { useAnalytics } from '@launch77-shared/lib-analytics-web'
|
|
87
|
+
|
|
88
|
+
function MyComponent() {
|
|
89
|
+
const { trackClick, trackPageView, trackCustom } = useAnalytics()
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<button
|
|
93
|
+
onClick={() =>
|
|
94
|
+
trackClick({
|
|
95
|
+
action: 'click_cta',
|
|
96
|
+
category: 'marketing',
|
|
97
|
+
label: 'Hero CTA',
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
>
|
|
101
|
+
Get Started
|
|
102
|
+
</button>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Trackable Components
|
|
108
|
+
|
|
109
|
+
Use pre-built components for automatic tracking:
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
import { TrackableButton, TrackableLink } from '@launch77-shared/lib-analytics-web'
|
|
113
|
+
|
|
114
|
+
function HeroSection() {
|
|
115
|
+
return (
|
|
116
|
+
<>
|
|
117
|
+
<TrackableButton eventAction="click_signup" eventCategory="conversion" eventLabel="Hero Signup Button" className="bg-primary text-primary-foreground px-6 py-3 rounded-lg">
|
|
118
|
+
Sign Up Now
|
|
119
|
+
</TrackableButton>
|
|
120
|
+
|
|
121
|
+
<TrackableLink href="/pricing" eventAction="click_pricing" eventCategory="navigation" eventLabel="Hero Pricing Link" className="text-primary hover:underline">
|
|
122
|
+
View Pricing
|
|
123
|
+
</TrackableLink>
|
|
124
|
+
</>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Page View Tracking
|
|
130
|
+
|
|
131
|
+
Auto-track SPA navigation:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
'use client'
|
|
135
|
+
|
|
136
|
+
import { usePageTracking } from '@launch77-shared/lib-analytics-web'
|
|
137
|
+
|
|
138
|
+
export default function Layout({ children }) {
|
|
139
|
+
// Automatically tracks page changes
|
|
140
|
+
usePageTracking()
|
|
141
|
+
|
|
142
|
+
return <>{children}</>
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Form Tracking
|
|
147
|
+
|
|
148
|
+
Track form submissions:
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
'use client'
|
|
152
|
+
|
|
153
|
+
import { useAnalytics } from '@launch77-shared/lib-analytics-web'
|
|
154
|
+
|
|
155
|
+
function ContactForm() {
|
|
156
|
+
const { trackForm } = useAnalytics()
|
|
157
|
+
|
|
158
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
159
|
+
e.preventDefault()
|
|
160
|
+
|
|
161
|
+
// Track form submission
|
|
162
|
+
trackForm({
|
|
163
|
+
action: 'submit',
|
|
164
|
+
category: 'lead_generation',
|
|
165
|
+
label: 'Contact Form',
|
|
166
|
+
formName: 'contact_form',
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Your form submission logic...
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return <form onSubmit={handleSubmit}>{/* Form fields */}</form>
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Custom Events
|
|
177
|
+
|
|
178
|
+
Track any custom event with metadata:
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
import { useAnalytics } from '@launch77-shared/lib-analytics-web'
|
|
182
|
+
|
|
183
|
+
function VideoPlayer() {
|
|
184
|
+
const { trackCustom } = useAnalytics()
|
|
185
|
+
|
|
186
|
+
const handlePlay = () => {
|
|
187
|
+
trackCustom({
|
|
188
|
+
action: 'video_play',
|
|
189
|
+
category: 'engagement',
|
|
190
|
+
label: 'Product Demo Video',
|
|
191
|
+
metadata: {
|
|
192
|
+
video_id: 'demo_v1',
|
|
193
|
+
duration: 120,
|
|
194
|
+
timestamp: new Date().toISOString(),
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return <video onPlay={handlePlay}>...</video>
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Scroll Tracking
|
|
204
|
+
|
|
205
|
+
Track user scroll depth:
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
'use client'
|
|
209
|
+
|
|
210
|
+
import { useScrollTracking } from '@launch77-shared/lib-analytics-web'
|
|
211
|
+
|
|
212
|
+
export default function BlogPost() {
|
|
213
|
+
// Tracks when user scrolls to 25%, 50%, 75%, 100%
|
|
214
|
+
useScrollTracking()
|
|
215
|
+
|
|
216
|
+
return <article>{/* Your content */}</article>
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Consent Management
|
|
221
|
+
|
|
222
|
+
### Checking Consent Status
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
import { useAnalytics } from '@launch77-shared/lib-analytics-web'
|
|
226
|
+
|
|
227
|
+
function MyComponent() {
|
|
228
|
+
const { consent, hasConsent } = useAnalytics()
|
|
229
|
+
|
|
230
|
+
if (consent === 'NOT_SET') {
|
|
231
|
+
return <p>Please accept cookies to use this feature</p>
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!hasConsent()) {
|
|
235
|
+
return <p>Analytics is disabled</p>
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// User has consented, show full features
|
|
239
|
+
return <div>...</div>
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Programmatic Consent
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
import { useAnalytics } from '@launch77-shared/lib-analytics-web'
|
|
247
|
+
|
|
248
|
+
function SettingsPage() {
|
|
249
|
+
const { saveConsent, consent } = useAnalytics()
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<div>
|
|
253
|
+
<h2>Privacy Settings</h2>
|
|
254
|
+
<button onClick={() => saveConsent(true)}>Enable Analytics</button>
|
|
255
|
+
<button onClick={() => saveConsent(false)}>Disable Analytics</button>
|
|
256
|
+
<p>Current: {consent}</p>
|
|
257
|
+
</div>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Privacy & GDPR
|
|
263
|
+
|
|
264
|
+
### How Consent Works
|
|
265
|
+
|
|
266
|
+
1. **First Visit**: `consent === 'NOT_SET'` - No tracking occurs
|
|
267
|
+
2. **User Accepts**: `consent === 'GRANTED'` - Full tracking enabled
|
|
268
|
+
3. **User Declines**: `consent === 'DENIED'` - No tracking, saved in localStorage
|
|
269
|
+
4. **Persistence**: Preference saved in `localStorage` as `analytics_consent`
|
|
270
|
+
|
|
271
|
+
### PII Sanitization
|
|
272
|
+
|
|
273
|
+
The library automatically sanitizes potentially sensitive data:
|
|
274
|
+
|
|
275
|
+
- Email addresses
|
|
276
|
+
- Phone numbers
|
|
277
|
+
- Credit card numbers
|
|
278
|
+
- Social security numbers
|
|
279
|
+
|
|
280
|
+
### Google Consent Mode
|
|
281
|
+
|
|
282
|
+
Automatically integrates with Google Consent Mode v2 for GDPR compliance.
|
|
283
|
+
|
|
284
|
+
### Data Collected
|
|
285
|
+
|
|
286
|
+
When consent is granted, the library tracks:
|
|
287
|
+
|
|
288
|
+
- ✅ Page URLs and paths
|
|
289
|
+
- ✅ Button/link clicks
|
|
290
|
+
- ✅ Form submissions
|
|
291
|
+
- ✅ Custom events
|
|
292
|
+
- ✅ User interactions
|
|
293
|
+
- ✅ Anonymous user ID (generated by PostHog/GTM)
|
|
294
|
+
|
|
295
|
+
When consent is denied:
|
|
296
|
+
|
|
297
|
+
- ❌ No tracking occurs
|
|
298
|
+
- ❌ No analytics scripts loaded
|
|
299
|
+
- ❌ No data sent to third parties
|
|
300
|
+
|
|
301
|
+
## Advanced Usage
|
|
302
|
+
|
|
303
|
+
### Conditional Analytics
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
import { useAnalytics } from '@launch77-shared/lib-analytics-web'
|
|
307
|
+
|
|
308
|
+
function FeatureButton() {
|
|
309
|
+
const { hasConsent, trackClick } = useAnalytics()
|
|
310
|
+
|
|
311
|
+
const handleClick = () => {
|
|
312
|
+
// Only track if user consented
|
|
313
|
+
if (hasConsent()) {
|
|
314
|
+
trackClick({
|
|
315
|
+
action: 'feature_click',
|
|
316
|
+
category: 'engagement',
|
|
317
|
+
label: 'Premium Feature',
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Always execute feature logic
|
|
322
|
+
performFeatureAction()
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return <button onClick={handleClick}>Use Feature</button>
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Environment-Based Config
|
|
330
|
+
|
|
331
|
+
```tsx
|
|
332
|
+
// Only enable analytics in production
|
|
333
|
+
const isProduction = process.env.NODE_ENV === 'production'
|
|
334
|
+
const analyticsEnabled = process.env.NEXT_PUBLIC_ANALYTICS_ENABLED !== 'false'
|
|
335
|
+
|
|
336
|
+
<AnalyticsProvider
|
|
337
|
+
gtmId={isProduction ? process.env.NEXT_PUBLIC_GTM_ID : undefined}
|
|
338
|
+
posthogKey={analyticsEnabled ? process.env.NEXT_PUBLIC_POSTHOG_KEY : undefined}
|
|
339
|
+
>
|
|
340
|
+
{children}
|
|
341
|
+
</AnalyticsProvider>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Self-Hosted PostHog
|
|
345
|
+
|
|
346
|
+
```tsx
|
|
347
|
+
<AnalyticsProvider
|
|
348
|
+
gtmId={process.env.NEXT_PUBLIC_GTM_ID}
|
|
349
|
+
posthogKey={process.env.NEXT_PUBLIC_POSTHOG_KEY}
|
|
350
|
+
posthogConfig={{
|
|
351
|
+
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',
|
|
352
|
+
}}
|
|
353
|
+
>
|
|
354
|
+
{children}
|
|
355
|
+
</AnalyticsProvider>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Debugging
|
|
359
|
+
|
|
360
|
+
### Browser Console
|
|
361
|
+
|
|
362
|
+
In development mode, all events are logged to the console:
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
[Analytics] Page View: /home
|
|
366
|
+
[Analytics] Click Event: { action: 'click_cta', category: 'marketing' }
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### PostHog Toolbar
|
|
370
|
+
|
|
371
|
+
Install the PostHog toolbar for real-time event debugging:
|
|
372
|
+
|
|
373
|
+
1. Add `?ph_debug=true` to your URL
|
|
374
|
+
2. Or enable in PostHog project settings
|
|
375
|
+
3. See events as they're sent
|
|
376
|
+
|
|
377
|
+
### Google Tag Manager Preview
|
|
378
|
+
|
|
379
|
+
1. Go to [https://tagmanager.google.com](https://tagmanager.google.com)
|
|
380
|
+
2. Click "Preview" in your container
|
|
381
|
+
3. Enter your localhost URL
|
|
382
|
+
4. See events in real-time
|
|
383
|
+
|
|
384
|
+
### Network Tab
|
|
385
|
+
|
|
386
|
+
Filter by:
|
|
387
|
+
|
|
388
|
+
- `collect` - Google Analytics events
|
|
389
|
+
- `event` - PostHog events
|
|
390
|
+
- `gtm` - GTM script loads
|
|
391
|
+
|
|
392
|
+
## Troubleshooting
|
|
393
|
+
|
|
394
|
+
### Events Not Appearing
|
|
395
|
+
|
|
396
|
+
**Issue:** Events aren't showing up in PostHog/GTM.
|
|
397
|
+
|
|
398
|
+
**Solutions:**
|
|
399
|
+
|
|
400
|
+
1. Check consent status - events only fire when `consent === 'GRANTED'`
|
|
401
|
+
2. Verify environment variables are set correctly
|
|
402
|
+
3. Check browser console for errors
|
|
403
|
+
4. Ensure AnalyticsProvider is wrapping your app
|
|
404
|
+
5. Try clearing localStorage and re-consenting
|
|
405
|
+
|
|
406
|
+
### Consent Banner Not Showing
|
|
407
|
+
|
|
408
|
+
**Issue:** ConsentBanner component doesn't appear.
|
|
409
|
+
|
|
410
|
+
**Solutions:**
|
|
411
|
+
|
|
412
|
+
1. Import and render `<ConsentBanner />` in your root layout
|
|
413
|
+
2. Check if consent is already set in localStorage (clear to test)
|
|
414
|
+
3. Verify component is in a Client Component (`'use client'`)
|
|
415
|
+
|
|
416
|
+
### TypeScript Errors
|
|
417
|
+
|
|
418
|
+
**Issue:** Type errors with analytics functions.
|
|
419
|
+
|
|
420
|
+
**Solutions:**
|
|
421
|
+
|
|
422
|
+
1. Ensure `@launch77-shared/lib-analytics-web` is installed
|
|
423
|
+
2. Restart TypeScript server in VS Code
|
|
424
|
+
3. Check that types are exported from library
|
|
425
|
+
|
|
426
|
+
### Build Failures
|
|
427
|
+
|
|
428
|
+
**Issue:** Build fails with module not found.
|
|
429
|
+
|
|
430
|
+
**Solutions:**
|
|
431
|
+
|
|
432
|
+
1. Run `npm install` at workspace root
|
|
433
|
+
2. Verify library is in package.json dependencies
|
|
434
|
+
3. Check that paths are correct in imports
|
|
435
|
+
4. Restart dev server
|
|
436
|
+
|
|
437
|
+
## Best Practices
|
|
438
|
+
|
|
439
|
+
### Do's ✅
|
|
440
|
+
|
|
441
|
+
- Always check consent before showing analytics-dependent features
|
|
442
|
+
- Use trackable components for consistent tracking
|
|
443
|
+
- Add meaningful labels and categories to events
|
|
444
|
+
- Test analytics in production-like environment
|
|
445
|
+
- Document custom events for your team
|
|
446
|
+
- Use TypeScript for type safety
|
|
447
|
+
|
|
448
|
+
### Don'ts ❌
|
|
449
|
+
|
|
450
|
+
- Don't track without consent
|
|
451
|
+
- Don't include PII in event metadata
|
|
452
|
+
- Don't track sensitive user actions
|
|
453
|
+
- Don't hardcode analytics IDs in code
|
|
454
|
+
- Don't skip testing consent flow
|
|
455
|
+
- Don't forget to update privacy policy
|
|
456
|
+
|
|
457
|
+
## Event Naming Conventions
|
|
458
|
+
|
|
459
|
+
Follow consistent naming for better analytics:
|
|
460
|
+
|
|
461
|
+
```tsx
|
|
462
|
+
// ✅ Good - Clear action, category, label
|
|
463
|
+
trackClick({
|
|
464
|
+
action: 'click_signup_button',
|
|
465
|
+
category: 'conversion',
|
|
466
|
+
label: 'Header Signup CTA',
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
// ❌ Bad - Vague, inconsistent
|
|
470
|
+
trackClick({
|
|
471
|
+
action: 'clicked',
|
|
472
|
+
category: 'button',
|
|
473
|
+
label: 'Signup',
|
|
474
|
+
})
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Recommended Structure
|
|
478
|
+
|
|
479
|
+
- **Action**: `verb_noun_context` (e.g., `click_button_submit`, `view_page_pricing`)
|
|
480
|
+
- **Category**: Business domain (e.g., `conversion`, `engagement`, `navigation`)
|
|
481
|
+
- **Label**: Specific identifier (e.g., `Hero CTA`, `Footer Link`, `Mobile Menu`)
|
|
482
|
+
|
|
483
|
+
## Integration with Other Tools
|
|
484
|
+
|
|
485
|
+
### Google Analytics 4
|
|
486
|
+
|
|
487
|
+
Configure GA4 tags in GTM to receive events from the analytics library.
|
|
488
|
+
|
|
489
|
+
### Segment
|
|
490
|
+
|
|
491
|
+
You can wrap this library's events and forward to Segment if needed.
|
|
492
|
+
|
|
493
|
+
### Mixpanel
|
|
494
|
+
|
|
495
|
+
PostHog can export data to Mixpanel via integrations.
|
|
496
|
+
|
|
497
|
+
### Custom Analytics
|
|
498
|
+
|
|
499
|
+
Use `trackCustom()` to send events to any analytics platform:
|
|
500
|
+
|
|
501
|
+
```tsx
|
|
502
|
+
const { trackCustom } = useAnalytics()
|
|
503
|
+
|
|
504
|
+
trackCustom({
|
|
505
|
+
action: 'my_event',
|
|
506
|
+
category: 'custom',
|
|
507
|
+
label: 'My Label',
|
|
508
|
+
metadata: {
|
|
509
|
+
// Your custom metadata
|
|
510
|
+
},
|
|
511
|
+
})
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
## Performance
|
|
515
|
+
|
|
516
|
+
### Bundle Size
|
|
517
|
+
|
|
518
|
+
The analytics library is tree-shakeable and only includes:
|
|
519
|
+
|
|
520
|
+
- PostHog JS (~60KB gzipped)
|
|
521
|
+
- GTM via @next/third-parties (optimized loading)
|
|
522
|
+
|
|
523
|
+
### Loading Strategy
|
|
524
|
+
|
|
525
|
+
- GTM script loads asynchronously
|
|
526
|
+
- PostHog initializes after consent
|
|
527
|
+
- No blocking of page render
|
|
528
|
+
- Minimal performance impact
|
|
529
|
+
|
|
530
|
+
## Next Steps
|
|
531
|
+
|
|
532
|
+
1. **Configure Platforms** - Add GTM and PostHog credentials to `.env.local`
|
|
533
|
+
2. **Test Tracking** - Visit `/analytics-test` and verify events
|
|
534
|
+
3. **Customize Consent Banner** - Update `ConsentBanner.tsx` with your brand
|
|
535
|
+
4. **Add Tracking** - Use hooks and components throughout your app
|
|
536
|
+
5. **Set Up Dashboards** - Create PostHog/GTM dashboards for your events
|
|
537
|
+
6. **Update Privacy Policy** - Document analytics usage in privacy policy
|
|
538
|
+
7. **Train Team** - Share event naming conventions with developers
|
|
539
|
+
|
|
540
|
+
## Resources
|
|
541
|
+
|
|
542
|
+
- **Library Documentation**: `node_modules/@launch77-shared/lib-analytics-web/README.md`
|
|
543
|
+
- **PostHog Docs**: [https://posthog.com/docs](https://posthog.com/docs)
|
|
544
|
+
- **GTM Docs**: [https://developers.google.com/tag-manager](https://developers.google.com/tag-manager)
|
|
545
|
+
- **Google Consent Mode**: [https://support.google.com/tagmanager/answer/10718549](https://support.google.com/tagmanager/answer/10718549)
|
|
546
|
+
- **GDPR Compliance**: [https://gdpr.eu](https://gdpr.eu)
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
Need help? Check the analytics library README or ask in the Launch77 community.
|