@neomanex/analytics-nuxt 1.0.2

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,54 @@
1
+ # @neomanex/analytics
2
+
3
+ Nuxt module for analytics tracking with SSR middleware, browser plugin, and server proxy.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ pnpm add @neomanex/analytics
9
+ ```
10
+
11
+ ```typescript
12
+ // nuxt.config.ts
13
+ export default defineNuxtConfig({
14
+ modules: ['@neomanex/analytics'],
15
+
16
+ analytics: {
17
+ apiUrl: 'https://analytics-api.neomanex.com',
18
+ source: 'my-app',
19
+ },
20
+ })
21
+ ```
22
+
23
+ ## Features
24
+
25
+ - **Server-side tracking** - Accurate analytics even with ad blockers
26
+ - **Automatic events** - `page.visit`, `page.view`, `session.start`, `page.leave`
27
+ - **Manual tracking** - `useAnalytics()` composable for custom events
28
+ - **Correlation IDs** - Links SSR and browser events across the session
29
+ - **Secure proxy** - API URL never exposed to browser
30
+
31
+ ## Full Documentation
32
+
33
+ For complete documentation including installation, configuration, API reference, and guides:
34
+
35
+ **[View Full Documentation →](./docs/01-overview/01-introduction.md)**
36
+
37
+ Key sections:
38
+ - [Introduction](./docs/01-overview/01-introduction.md) - What, why, and architecture
39
+ - [Installation](./docs/01-overview/02-installation.md) - Setup and configuration
40
+ - [API Reference](./docs/02-api/) - Coming soon
41
+ - [Guides](./docs/03-guides/) - Coming soon
42
+
43
+ ## Development
44
+
45
+ ```bash
46
+ npm install # Install dependencies
47
+ npm test # Run tests
48
+ npm run dev # Dev playground
49
+ npm run build # Build module
50
+ ```
51
+
52
+ ## License
53
+
54
+ UNLICENSED
@@ -0,0 +1,5 @@
1
+ module.exports = function(...args) {
2
+ return import('./module.mjs').then(m => m.default.call(this, ...args))
3
+ }
4
+ const _meta = module.exports.meta = require('./module.json')
5
+ module.exports.getMeta = () => Promise.resolve(_meta)
@@ -0,0 +1,6 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+ import { ModuleOptions } from '../dist/runtime/types.js';
3
+
4
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
5
+
6
+ export { _default as default };
@@ -0,0 +1,6 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+ import { ModuleOptions } from '../dist/runtime/types.js';
3
+
4
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
5
+
6
+ export { _default as default };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@neomanex/analytics",
3
+ "configKey": "analytics",
4
+ "compatibility": {
5
+ "nuxt": ">=3.10.0"
6
+ },
7
+ "version": "1.0.0",
8
+ "builder": {
9
+ "@nuxt/module-builder": "0.8.4",
10
+ "unbuild": "unknown"
11
+ }
12
+ }
@@ -0,0 +1,86 @@
1
+ import { defineNuxtModule, createResolver, addServerHandler, addPlugin, addImports, addTypeTemplate } from '@nuxt/kit';
2
+ import { defu } from 'defu';
3
+
4
+ const module = defineNuxtModule({
5
+ meta: {
6
+ name: "@neomanex/analytics",
7
+ configKey: "analytics",
8
+ compatibility: {
9
+ nuxt: ">=3.10.0"
10
+ },
11
+ version: "1.0.0"
12
+ },
13
+ defaults: {
14
+ apiUrl: "",
15
+ source: "",
16
+ trackPageVisits: true,
17
+ trackPageViews: true,
18
+ trackPageLeave: true,
19
+ excludePaths: []
20
+ },
21
+ setup(options, nuxt) {
22
+ const resolver = createResolver(import.meta.url);
23
+ nuxt.options.runtimeConfig.analytics = defu(
24
+ nuxt.options.runtimeConfig.analytics || {},
25
+ {
26
+ apiUrl: options.apiUrl,
27
+ source: options.source,
28
+ trackPageVisits: options.trackPageVisits,
29
+ trackPageViews: options.trackPageViews,
30
+ trackPageLeave: options.trackPageLeave,
31
+ excludePaths: options.excludePaths
32
+ }
33
+ );
34
+ nuxt.options.runtimeConfig.public.analytics = defu(
35
+ nuxt.options.runtimeConfig.public.analytics || {},
36
+ {
37
+ source: options.source,
38
+ trackPageViews: options.trackPageViews,
39
+ trackPageLeave: options.trackPageLeave,
40
+ excludePaths: options.excludePaths
41
+ }
42
+ );
43
+ addServerHandler({
44
+ handler: resolver.resolve("./runtime/server/middleware/page-visit"),
45
+ middleware: true
46
+ });
47
+ addServerHandler({
48
+ route: "/api/_analytics",
49
+ handler: resolver.resolve("./runtime/server/api/_analytics.post")
50
+ });
51
+ addPlugin({
52
+ src: resolver.resolve("./runtime/plugin.server"),
53
+ mode: "server"
54
+ });
55
+ addPlugin({
56
+ src: resolver.resolve("./runtime/plugin.client"),
57
+ mode: "client"
58
+ });
59
+ addImports({
60
+ name: "useAnalytics",
61
+ from: resolver.resolve("./runtime/app/composables/useAnalytics")
62
+ });
63
+ addTypeTemplate({
64
+ filename: "types/analytics.d.ts",
65
+ getContents: () => `
66
+ import type { AnalyticsClient } from '${resolver.resolve("./runtime/client")}'
67
+
68
+ declare module '#app' {
69
+ interface NuxtApp {
70
+ $analytics: AnalyticsClient
71
+ }
72
+ }
73
+
74
+ declare module 'vue' {
75
+ interface ComponentCustomProperties {
76
+ $analytics: AnalyticsClient
77
+ }
78
+ }
79
+
80
+ export {}
81
+ `
82
+ });
83
+ }
84
+ });
85
+
86
+ export { module as default };
File without changes
@@ -0,0 +1,8 @@
1
+ import { useNuxtApp } from "#imports";
2
+ export function useAnalytics() {
3
+ const { $analytics } = useNuxtApp();
4
+ return {
5
+ track: $analytics.track.bind($analytics),
6
+ flush: $analytics.flush.bind($analytics)
7
+ };
8
+ }
File without changes
@@ -0,0 +1,170 @@
1
+ import { $fetch } from "ofetch";
2
+ import { resolveConfig, OVERFLOW_WARNING_DEBOUNCE_MS } from "./config.js";
3
+ export class AnalyticsClient {
4
+ config;
5
+ queue = [];
6
+ flushTimer = null;
7
+ isFlushing = false;
8
+ isShutdown = false;
9
+ /** Debounce state for queue overflow warnings */
10
+ _lastOverflowWarning = 0;
11
+ _overflowWarningShown = false;
12
+ constructor(config) {
13
+ this.config = resolveConfig(config);
14
+ this.startFlushTimer();
15
+ }
16
+ /**
17
+ * Add an event to the queue. Returns immediately and never throws.
18
+ */
19
+ track(eventType, metadata, options) {
20
+ if (this.isShutdown) {
21
+ return;
22
+ }
23
+ try {
24
+ const event = {
25
+ event_type: eventType,
26
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
27
+ source: this.config.source,
28
+ metadata: metadata ?? void 0,
29
+ account_id: options?.account_id,
30
+ user_id: options?.user_id
31
+ };
32
+ if (this.queue.length >= this.config.maxQueueSize) {
33
+ this.handleQueueOverflow(event);
34
+ return;
35
+ }
36
+ this.queue.push(event);
37
+ if (this.queue.length >= this.config.flushSize) {
38
+ void this.flush();
39
+ }
40
+ } catch {
41
+ }
42
+ }
43
+ /**
44
+ * Manually flush all queued events to the batch endpoint.
45
+ */
46
+ async flush() {
47
+ if (this.isFlushing || this.queue.length === 0) {
48
+ return;
49
+ }
50
+ this.isFlushing = true;
51
+ const events = this.queue.splice(0, this.queue.length);
52
+ try {
53
+ await this.flushWithRetry(events);
54
+ } catch {
55
+ } finally {
56
+ this.isFlushing = false;
57
+ }
58
+ }
59
+ /**
60
+ * Flush remaining events and stop the timer. Call on process exit.
61
+ */
62
+ async shutdown() {
63
+ this.isShutdown = true;
64
+ this.stopFlushTimer();
65
+ if (this.queue.length > 0) {
66
+ await this.flush();
67
+ }
68
+ }
69
+ /**
70
+ * Get current queue length (for testing/monitoring).
71
+ */
72
+ get queueLength() {
73
+ return this.queue.length;
74
+ }
75
+ /**
76
+ * Build an event payload for sendBeacon usage (browser page.leave).
77
+ * Returns the JSON string to send via navigator.sendBeacon.
78
+ */
79
+ buildBeaconPayload(eventType, metadata) {
80
+ const event = {
81
+ event_type: eventType,
82
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
83
+ source: this.config.source,
84
+ metadata
85
+ };
86
+ return JSON.stringify({ events: [event] });
87
+ }
88
+ /**
89
+ * Send a batch of events to the API with exponential backoff retry.
90
+ */
91
+ async flushWithRetry(events) {
92
+ let attempt = 0;
93
+ let delay = this.config.retryDelay;
94
+ const maxRetries = this.config.maxRetries;
95
+ const backoffMultiplier = this.config.retryBackoffMultiplier;
96
+ while (attempt <= maxRetries) {
97
+ try {
98
+ await this.sendBatch(events);
99
+ return;
100
+ } catch (error) {
101
+ attempt++;
102
+ if (attempt > maxRetries) {
103
+ if (this.config.onError) {
104
+ this.config.onError(error, events);
105
+ }
106
+ throw error;
107
+ }
108
+ await new Promise((resolve) => setTimeout(resolve, delay));
109
+ delay *= backoffMultiplier;
110
+ }
111
+ }
112
+ }
113
+ /**
114
+ * Send a batch of events via ofetch.
115
+ */
116
+ async sendBatch(events) {
117
+ await $fetch("/api/v1/events/batch", {
118
+ baseURL: this.config.apiUrl,
119
+ method: "POST",
120
+ body: { events }
121
+ });
122
+ }
123
+ /**
124
+ * Handle queue overflow: drop oldest event, push new one, warn with debounce.
125
+ */
126
+ handleQueueOverflow(event) {
127
+ this.queue.shift();
128
+ const now = Date.now();
129
+ if (!this._overflowWarningShown || now - this._lastOverflowWarning > OVERFLOW_WARNING_DEBOUNCE_MS) {
130
+ console.warn(
131
+ "[Analytics] Queue overflow: dropping events. Increase maxQueueSize or reduce event frequency."
132
+ );
133
+ this._lastOverflowWarning = now;
134
+ this._overflowWarningShown = true;
135
+ }
136
+ this.queue.push(event);
137
+ }
138
+ startFlushTimer() {
139
+ this.flushTimer = setInterval(() => {
140
+ void this.flush();
141
+ }, this.config.flushInterval);
142
+ }
143
+ stopFlushTimer() {
144
+ if (this.flushTimer) {
145
+ clearInterval(this.flushTimer);
146
+ this.flushTimer = null;
147
+ }
148
+ }
149
+ }
150
+ let clientInstance = null;
151
+ export function init(config) {
152
+ if (clientInstance) {
153
+ console.warn("[Analytics] Already initialized");
154
+ return clientInstance;
155
+ }
156
+ clientInstance = new AnalyticsClient(config);
157
+ return clientInstance;
158
+ }
159
+ export function getInstance() {
160
+ if (!clientInstance) {
161
+ throw new Error("[Analytics] Not initialized. Call init() first.");
162
+ }
163
+ return clientInstance;
164
+ }
165
+ export function resetInstance() {
166
+ if (clientInstance) {
167
+ void clientInstance.shutdown();
168
+ }
169
+ clientInstance = null;
170
+ }
File without changes
@@ -0,0 +1,20 @@
1
+ export const DEFAULT_FLUSH_INTERVAL = 1e4;
2
+ export const DEFAULT_FLUSH_SIZE = 10;
3
+ export const DEFAULT_MAX_QUEUE_SIZE = 1e3;
4
+ export const DEFAULT_MAX_RETRIES = 3;
5
+ export const DEFAULT_RETRY_DELAY = 1e3;
6
+ export const DEFAULT_RETRY_BACKOFF_MULTIPLIER = 2;
7
+ export const OVERFLOW_WARNING_DEBOUNCE_MS = 6e4;
8
+ export function resolveConfig(config) {
9
+ return {
10
+ apiUrl: config.apiUrl,
11
+ source: config.source,
12
+ flushInterval: config.flushInterval ?? DEFAULT_FLUSH_INTERVAL,
13
+ flushSize: config.flushSize ?? DEFAULT_FLUSH_SIZE,
14
+ maxQueueSize: config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
15
+ maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
16
+ retryDelay: config.retryDelay ?? DEFAULT_RETRY_DELAY,
17
+ retryBackoffMultiplier: config.retryBackoffMultiplier ?? DEFAULT_RETRY_BACKOFF_MULTIPLIER,
18
+ onError: config.onError
19
+ };
20
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Type declarations for #imports stub.
3
+ *
4
+ * Provides type signatures matching Nuxt's auto-imports for development
5
+ * and type checking outside of a Nuxt context.
6
+ */
7
+
8
+ export declare function defineEventHandler<T>(handler: (event: any) => T | Promise<T>): (event: any) => T | Promise<T>
9
+ export declare function defineNuxtPlugin(factory: (nuxtApp?: any) => any): any
10
+ export declare function getRequestHeader(event: any, name: string): string | undefined
11
+ export declare function setResponseHeader(event: any, name: string, value: string): void
12
+ export declare function useRuntimeConfig(): any
13
+ export declare function getRequestURL(event: any): URL
14
+ export declare function readBody<T = any>(event: any): Promise<T>
15
+ export declare function useRouter(): any
16
+ export declare function useNuxtApp(): any
@@ -0,0 +1,42 @@
1
+ export function defineEventHandler(handler) {
2
+ return handler;
3
+ }
4
+ export function defineNuxtPlugin(factory) {
5
+ return factory;
6
+ }
7
+ export function getRequestHeader(_event, _name) {
8
+ return void 0;
9
+ }
10
+ export function setResponseHeader(_event, _name, _value) {
11
+ }
12
+ export function useRuntimeConfig() {
13
+ return {};
14
+ }
15
+ export function getRequestURL(_event) {
16
+ return new URL("http://localhost/");
17
+ }
18
+ export function readBody(_event) {
19
+ return Promise.resolve({});
20
+ }
21
+ export function useRouter() {
22
+ return {
23
+ afterEach: () => {
24
+ }
25
+ };
26
+ }
27
+ export function useNuxtApp() {
28
+ return {
29
+ $analytics: {}
30
+ };
31
+ }
32
+ export function addServerHandler(_options) {
33
+ }
34
+ export function addPlugin(_options) {
35
+ }
36
+ export function addImports(_options) {
37
+ }
38
+ export function addTypeTemplate(_options) {
39
+ }
40
+ export function createResolver(_url) {
41
+ return { resolve: (path) => path };
42
+ }
File without changes
@@ -0,0 +1,98 @@
1
+ import { defineNuxtPlugin, useRouter, useRuntimeConfig } from "#imports";
2
+ import { init, getInstance } from "./client.js";
3
+ const SESSION_ID_KEY = "neomanex_analytics_session_id";
4
+ const PAGE_ENTRY_KEY = "neomanex_analytics_page_entry";
5
+ function generateUUID() {
6
+ return self.crypto.randomUUID();
7
+ }
8
+ function getOrCreateSessionId() {
9
+ let sessionId = sessionStorage.getItem(SESSION_ID_KEY);
10
+ if (!sessionId) {
11
+ sessionId = generateUUID();
12
+ sessionStorage.setItem(SESSION_ID_KEY, sessionId);
13
+ }
14
+ return sessionId;
15
+ }
16
+ function getCorrelationId() {
17
+ const meta = document.querySelector('meta[name="x-correlation-id"]');
18
+ return meta?.getAttribute("content") ?? void 0;
19
+ }
20
+ function getScrollDepthPct() {
21
+ const docHeight = document.documentElement.scrollHeight;
22
+ const winHeight = window.innerHeight;
23
+ const scrollTop = window.scrollY;
24
+ if (docHeight <= winHeight) {
25
+ return 100;
26
+ }
27
+ return Math.min(100, Math.round((scrollTop + winHeight) / docHeight * 100));
28
+ }
29
+ function shouldExclude(path, excludePaths) {
30
+ return excludePaths.some((pattern) => path.startsWith(pattern));
31
+ }
32
+ export default defineNuxtPlugin(() => {
33
+ const runtimeConfig = useRuntimeConfig();
34
+ const publicConfig = runtimeConfig.public.analytics;
35
+ let client;
36
+ try {
37
+ client = getInstance();
38
+ } catch {
39
+ client = init({
40
+ apiUrl: "/api/_analytics",
41
+ source: publicConfig.source,
42
+ flushInterval: 1e4,
43
+ flushSize: 5
44
+ });
45
+ }
46
+ const router = useRouter();
47
+ const sessionId = getOrCreateSessionId();
48
+ const correlationId = getCorrelationId();
49
+ let isFirstPageView = !sessionStorage.getItem(PAGE_ENTRY_KEY);
50
+ sessionStorage.setItem(PAGE_ENTRY_KEY, Date.now().toString());
51
+ if (isFirstPageView) {
52
+ client.track("session.start", {
53
+ entry_path: window.location.pathname,
54
+ screen_width: window.screen.width,
55
+ screen_height: window.screen.height,
56
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
57
+ language: navigator.language,
58
+ correlation_id: correlationId,
59
+ session_id: sessionId
60
+ });
61
+ isFirstPageView = false;
62
+ }
63
+ if (publicConfig.trackPageViews) {
64
+ router.afterEach((to, from) => {
65
+ if (shouldExclude(to.path, publicConfig.excludePaths ?? [])) {
66
+ return;
67
+ }
68
+ sessionStorage.setItem(PAGE_ENTRY_KEY, Date.now().toString());
69
+ client.track("page.view", {
70
+ path: to.path,
71
+ query: to.query,
72
+ title: typeof document !== "undefined" ? document.title : void 0,
73
+ from_path: from.path,
74
+ correlation_id: getCorrelationId(),
75
+ session_id: sessionId
76
+ });
77
+ });
78
+ }
79
+ if (publicConfig.trackPageLeave) {
80
+ window.addEventListener("beforeunload", () => {
81
+ const entryTime = parseInt(sessionStorage.getItem(PAGE_ENTRY_KEY) ?? "0", 10);
82
+ const durationMs = entryTime > 0 ? Date.now() - entryTime : 0;
83
+ const payload = client.buildBeaconPayload("page.leave", {
84
+ path: window.location.pathname,
85
+ duration_ms: durationMs,
86
+ scroll_depth_pct: getScrollDepthPct(),
87
+ correlation_id: getCorrelationId(),
88
+ session_id: sessionId
89
+ });
90
+ navigator.sendBeacon("/api/_analytics", payload);
91
+ });
92
+ }
93
+ return {
94
+ provide: {
95
+ analytics: client
96
+ }
97
+ };
98
+ });
File without changes
@@ -0,0 +1,23 @@
1
+ import { defineNuxtPlugin, useRuntimeConfig } from "#imports";
2
+ import { init, getInstance } from "./client.js";
3
+ export default defineNuxtPlugin(() => {
4
+ const config = useRuntimeConfig();
5
+ const analyticsConfig = config.analytics;
6
+ let client;
7
+ try {
8
+ client = getInstance();
9
+ } catch {
10
+ client = init({
11
+ apiUrl: analyticsConfig.apiUrl,
12
+ source: analyticsConfig.source,
13
+ // Server-side uses longer flush interval (events are fewer but more valuable)
14
+ flushInterval: 5e3,
15
+ flushSize: 20
16
+ });
17
+ }
18
+ return {
19
+ provide: {
20
+ analytics: client
21
+ }
22
+ };
23
+ });
File without changes
@@ -0,0 +1,60 @@
1
+ import { defineEventHandler, readBody, getRequestHeader, useRuntimeConfig } from "#imports";
2
+ import { $fetch } from "ofetch";
3
+ function extractClientIp(event) {
4
+ const forwarded = getRequestHeader(event, "x-forwarded-for");
5
+ if (forwarded) {
6
+ return forwarded.split(",")[0]?.trim();
7
+ }
8
+ const realIp = getRequestHeader(event, "x-real-ip");
9
+ if (realIp) {
10
+ return realIp;
11
+ }
12
+ const nodeReq = event.node?.req;
13
+ if (nodeReq?.socket?.remoteAddress) {
14
+ return nodeReq.socket.remoteAddress;
15
+ }
16
+ return void 0;
17
+ }
18
+ export default defineEventHandler(async (event) => {
19
+ const config = useRuntimeConfig();
20
+ const analyticsConfig = config.analytics;
21
+ let body;
22
+ try {
23
+ body = await readBody(event);
24
+ } catch {
25
+ return { ok: false, error: "Invalid request body" };
26
+ }
27
+ if (!body?.events || !Array.isArray(body.events) || body.events.length === 0) {
28
+ return { ok: false, error: "No events provided" };
29
+ }
30
+ const clientIp = extractClientIp(event);
31
+ const userAgent = getRequestHeader(event, "user-agent");
32
+ const referer = getRequestHeader(event, "referer");
33
+ const acceptLanguage = getRequestHeader(event, "accept-language");
34
+ const origin = getRequestHeader(event, "origin");
35
+ const correlationId = getRequestHeader(event, "x-correlation-id");
36
+ const enrichedEvents = body.events.map((evt) => ({
37
+ ...evt,
38
+ source: evt.source || analyticsConfig.source,
39
+ client_ip: clientIp,
40
+ user_agent: userAgent,
41
+ referer,
42
+ accept_language: acceptLanguage,
43
+ metadata: {
44
+ ...evt.metadata,
45
+ origin,
46
+ correlation_id: correlationId ?? evt.metadata?.correlation_id
47
+ }
48
+ }));
49
+ try {
50
+ const response = await $fetch("/api/v1/events/batch", {
51
+ baseURL: analyticsConfig.apiUrl,
52
+ method: "POST",
53
+ body: { events: enrichedEvents }
54
+ });
55
+ return { ok: true, ingested: response.ingested };
56
+ } catch (error) {
57
+ console.error("[Analytics] Failed to forward events:", error);
58
+ return { ok: false, error: "Analytics service unavailable" };
59
+ }
60
+ });
File without changes
@@ -0,0 +1,67 @@
1
+ import { defineEventHandler, getRequestHeader, setResponseHeader, useRuntimeConfig, getRequestURL } from "#imports";
2
+ import { getInstance } from "../../client.js";
3
+ const EXCLUDED_PREFIXES = ["/api/", "/_nuxt/", "/__nuxt_error"];
4
+ const EXCLUDED_EXACT = ["/favicon.ico", "/_health", "/healthz"];
5
+ function extractClientIp(event) {
6
+ const forwarded = getRequestHeader(event, "x-forwarded-for");
7
+ if (forwarded) {
8
+ return forwarded.split(",")[0]?.trim();
9
+ }
10
+ const realIp = getRequestHeader(event, "x-real-ip");
11
+ if (realIp) {
12
+ return realIp;
13
+ }
14
+ const nodeReq = event.node?.req;
15
+ if (nodeReq?.socket?.remoteAddress) {
16
+ return nodeReq.socket.remoteAddress;
17
+ }
18
+ return void 0;
19
+ }
20
+ function generateCorrelationId() {
21
+ return crypto.randomUUID();
22
+ }
23
+ export default defineEventHandler((event) => {
24
+ const config = useRuntimeConfig();
25
+ const analyticsConfig = config.analytics;
26
+ if (!analyticsConfig.trackPageVisits) {
27
+ return;
28
+ }
29
+ const url = getRequestURL(event);
30
+ const path = url.pathname;
31
+ if (EXCLUDED_PREFIXES.some((prefix) => path.startsWith(prefix))) {
32
+ return;
33
+ }
34
+ if (EXCLUDED_EXACT.includes(path)) {
35
+ return;
36
+ }
37
+ if (analyticsConfig.excludePaths?.some((p) => path.startsWith(p))) {
38
+ return;
39
+ }
40
+ const ext = path.split(".").pop();
41
+ if (ext && ["js", "css", "png", "jpg", "jpeg", "gif", "svg", "ico", "woff", "woff2", "ttf", "eot", "map"].includes(ext)) {
42
+ return;
43
+ }
44
+ const correlationId = generateCorrelationId();
45
+ setResponseHeader(event, "X-Correlation-Id", correlationId);
46
+ const clientIp = extractClientIp(event);
47
+ const userAgent = getRequestHeader(event, "user-agent");
48
+ const referer = getRequestHeader(event, "referer");
49
+ const acceptLanguage = getRequestHeader(event, "accept-language");
50
+ const query = {};
51
+ url.searchParams.forEach((value, key) => {
52
+ query[key] = value;
53
+ });
54
+ try {
55
+ const client = getInstance();
56
+ client.track("page.visit", {
57
+ path,
58
+ query: Object.keys(query).length > 0 ? query : void 0,
59
+ correlation_id: correlationId,
60
+ client_ip: clientIp,
61
+ user_agent: userAgent,
62
+ referer,
63
+ accept_language: acceptLanguage
64
+ });
65
+ } catch {
66
+ }
67
+ });
File without changes
File without changes
@@ -0,0 +1,7 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module.js'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module.js'
@@ -0,0 +1,7 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module'
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@neomanex/analytics-nuxt",
3
+ "version": "1.0.2",
4
+ "description": "Nuxt module for analytics tracking with SSR middleware, browser plugin, and server proxy",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/types.d.ts",
10
+ "import": "./dist/module.mjs",
11
+ "require": "./dist/module.cjs"
12
+ }
13
+ },
14
+ "main": "./dist/module.cjs",
15
+ "types": "./dist/types.d.ts",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "prepack": "nuxt-module-build build",
21
+ "dev": "nuxi dev playground",
22
+ "dev:build": "nuxi build playground",
23
+ "dev:prepare": "nuxt-module-build build --stub && nuxi prepare playground",
24
+ "build": "nuxt-module-build build",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest watch",
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "dependencies": {
30
+ "@nuxt/kit": "^3.10.0",
31
+ "defu": "^6.1.4",
32
+ "ofetch": "^1.4.1"
33
+ },
34
+ "devDependencies": {
35
+ "@nuxt/module-builder": "^0.8.0",
36
+ "@nuxt/schema": "^3.10.0",
37
+ "nuxt": "^3.10.0",
38
+ "typescript": "^5.5.0",
39
+ "vitest": "^2.0.0"
40
+ },
41
+ "peerDependencies": {
42
+ "nuxt": ">=3.10.0 || >=4.0.0"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://gitlab.com/neomanex/neoanalytics/analytics-nuxt.git"
47
+ }
48
+ }