@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 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,7 @@
1
+ {
2
+ "targets": ["app"],
3
+ "pluginDependencies": {},
4
+ "libraryDependencies": {
5
+ "@launch77-shared/lib-analytics-web": "^0.1.0"
6
+ }
7
+ }
@@ -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 &quot;collect&quot; or &quot;event&quot; 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.