@webticks/core 0.1.0

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,98 @@
1
+ # WebTicks Core - Usage Examples
2
+
3
+ ## Browser Usage (No Changes Required!)
4
+
5
+ Your existing React/Next.js packages continue to work exactly as before:
6
+
7
+ ```jsx
8
+ import WebTicksAnalytics from '@webticks/react';
9
+
10
+ function App() {
11
+ return (
12
+ <div>
13
+ <WebTicksAnalytics />
14
+ {/* Your app */}
15
+ </div>
16
+ );
17
+ }
18
+ ```
19
+
20
+ ## Server-Side Usage
21
+
22
+ ### Option 1: Express Middleware (Automatic)
23
+
24
+ ```javascript
25
+ import express from 'express';
26
+ import { createServerMiddleware } from '@webticks/core/server';
27
+
28
+ const app = express();
29
+
30
+ // Automatically track all HTTP requests
31
+ app.use(createServerMiddleware({
32
+ backendUrl: 'https://api.example.com/track'
33
+ }));
34
+
35
+ app.listen(3000);
36
+ ```
37
+
38
+ ### Option 2: Manual Tracking
39
+
40
+ ```javascript
41
+ import { AnalyticsTracker } from '@webticks/core/tracker';
42
+
43
+ const tracker = new AnalyticsTracker({
44
+ backendUrl: 'https://api.example.com/track'
45
+ });
46
+
47
+ // Track server requests
48
+ tracker.trackServerRequest({
49
+ method: 'GET',
50
+ path: '/api/users',
51
+ query: { page: 1 },
52
+ headers: { 'user-agent': 'Mozilla/5.0' }
53
+ });
54
+
55
+ // Track custom events
56
+ tracker.trackEvent('database_query', {
57
+ table: 'users',
58
+ duration: 45
59
+ });
60
+
61
+ // Send immediately (useful for serverless)
62
+ await tracker.sendQueue();
63
+ ```
64
+
65
+ ### Next.js Example
66
+
67
+ ```javascript
68
+ // pages/api/users.js
69
+ import { AnalyticsTracker } from '@webticks/core/tracker';
70
+
71
+ const tracker = new AnalyticsTracker({
72
+ backendUrl: process.env.ANALYTICS_URL
73
+ });
74
+
75
+ export default async function handler(req, res) {
76
+ tracker.trackServerRequest({
77
+ method: req.method,
78
+ path: req.url,
79
+ query: req.query,
80
+ headers: {
81
+ 'user-agent': req.headers['user-agent']
82
+ }
83
+ });
84
+
85
+ // Your API logic here
86
+ const data = await getUsers();
87
+
88
+ res.status(200).json(data);
89
+ }
90
+ ```
91
+
92
+ ## Key Benefits
93
+
94
+ ✅ **Centralized Logic**: All tracking logic lives in `@webticks/core`
95
+ ✅ **Environment Agnostic**: Works in browser and Node.js
96
+ ✅ **Zero Breaking Changes**: Existing packages work without modifications
97
+ ✅ **Easy Updates**: Fix bugs once, all environments benefit
98
+ ✅ **Flexible**: Works with any framework (Express, Koa, Fastify, serverless, etc.)
package/injector.js ADDED
@@ -0,0 +1,27 @@
1
+ import { AnalyticsTracker } from "./tracker.js";
2
+
3
+ export default function inject() {
4
+ // Only auto-inject in browser environments
5
+ if (typeof window === 'undefined') {
6
+ console.warn("webticks auto-inject skipped: Not in a browser environment.");
7
+ return;
8
+ }
9
+
10
+ if (window.webticks) {
11
+ console.warn("webticks tracker already initialized.");
12
+ return;
13
+ }
14
+
15
+ const config = {
16
+ backendUrl: "https://api.example.com/track"
17
+ };
18
+
19
+ const tracker = new AnalyticsTracker(config);
20
+ tracker.autoTrackPageViews();
21
+ window.webticks = tracker;
22
+ };
23
+
24
+ // Only auto-execute in browser
25
+ if (typeof window !== 'undefined') {
26
+ inject();
27
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@webticks/core",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight analytics library for modern web applications with seamless event tracking and page view monitoring",
5
+ "type": "module",
6
+ "main": "injector.js",
7
+ "module": "injector.js",
8
+ "exports": {
9
+ ".": "./injector.js",
10
+ "./inject": "./injector.js",
11
+ "./tracker": "./tracker.js",
12
+ "./platform-adapters": "./platform-adapters.js"
13
+ },
14
+ "files": [
15
+ "tracker.js",
16
+ "injector.js",
17
+ "platform-adapters.js",
18
+ "types.js",
19
+ "README.md"
20
+ ],
21
+ "keywords": [
22
+ "analytics",
23
+ "tracking",
24
+ "web-analytics",
25
+ "event-tracking",
26
+ "page-views",
27
+ "metrics",
28
+ "telemetry"
29
+ ],
30
+ "author": "Celerinc Team",
31
+ "license": "MPL-2.0",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/Celerinc/webticks.git",
35
+ "directory": "packages/core"
36
+ },
37
+ "homepage": "https://github.com/Celerinc/webticks#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/Celerinc/webticks/issues"
40
+ }
41
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Platform adapters to abstract browser vs Node.js environment differences
3
+ */
4
+
5
+ // Environment detection
6
+ export function isServer() {
7
+ return typeof window === 'undefined';
8
+ }
9
+
10
+ export function isBrowser() {
11
+ return typeof window !== 'undefined';
12
+ }
13
+
14
+ /**
15
+ * Browser-specific adapter
16
+ */
17
+ export class BrowserAdapter {
18
+ constructor() {
19
+ this.storage = window.localStorage;
20
+ }
21
+
22
+ // Generate or retrieve user ID
23
+ getUserId() {
24
+ let userId = this.storage.getItem('webticks_uid');
25
+ if (!userId) {
26
+ userId = crypto.randomUUID();
27
+ this.storage.setItem('webticks_uid', userId);
28
+ }
29
+ return userId;
30
+ }
31
+
32
+ // Send HTTP request
33
+ async sendRequest(url, data, appId) {
34
+ const headers = { 'Content-Type': 'application/json' };
35
+
36
+ // Add webticks-app-id header if appId is provided
37
+ if (appId) {
38
+ headers['webticks-app-id'] = appId;
39
+ }
40
+
41
+ return fetch(url, {
42
+ method: 'POST',
43
+ headers: headers,
44
+ body: JSON.stringify(data)
45
+ }).catch((err) => console.warn('Error sending request:', err));
46
+ }
47
+
48
+ // Get current path
49
+ getCurrentPath() {
50
+ return window.location.href;
51
+ }
52
+
53
+ // Setup auto-tracking (browser-specific)
54
+ setupAutoTracking(tracker) {
55
+ // Patch history API
56
+ tracker.originalPushState = window.history.pushState;
57
+ tracker.originalReplaceState = window.history.replaceState;
58
+
59
+ window.history.pushState = (...args) => {
60
+ const result = tracker.originalPushState.apply(window.history, args);
61
+ tracker.checkPageChange();
62
+ if (typeof window.history.onpushstate === "function") {
63
+ window.history.onpushstate({ state: args[0] });
64
+ }
65
+ return result;
66
+ };
67
+
68
+ window.history.replaceState = (...args) => {
69
+ const result = tracker.originalReplaceState.apply(window.history, args);
70
+ tracker.checkPageChange();
71
+ if (typeof window.history.onreplacestate === "function") {
72
+ window.history.onreplacestate({ state: args[0] });
73
+ }
74
+ return result;
75
+ };
76
+
77
+ window.addEventListener('popstate', tracker.checkPageChange);
78
+ document.addEventListener('visibilitychange', tracker.handleVisibilityChange);
79
+ window.addEventListener('pagehide', tracker.handlePageHide);
80
+
81
+ // Track initial page view
82
+ tracker.lastPath = window.location.href;
83
+ tracker.trackPageView(tracker.lastPath);
84
+ }
85
+
86
+ // Cleanup auto-tracking
87
+ cleanupAutoTracking(tracker) {
88
+ if (tracker.originalPushState) {
89
+ window.history.pushState = tracker.originalPushState;
90
+ }
91
+ if (tracker.originalReplaceState) {
92
+ window.history.replaceState = tracker.originalReplaceState;
93
+ }
94
+ window.removeEventListener('popstate', tracker.checkPageChange);
95
+ document.removeEventListener('visibilitychange', tracker.handleVisibilityChange);
96
+ window.removeEventListener('pagehide', tracker.handlePageHide);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Factory function to get the appropriate adapter
102
+ * Core package only provides browser adapter
103
+ * For Node.js, use @webticks/node package
104
+ */
105
+ export function getPlatformAdapter() {
106
+ if (isBrowser()) {
107
+ return new BrowserAdapter();
108
+ }
109
+ // For server-side, users should use @webticks/node package
110
+ console.warn('Running in server environment. Please use @webticks/node package for server-side tracking.');
111
+ return null;
112
+ }
package/tracker.js ADDED
@@ -0,0 +1,265 @@
1
+ import { getPlatformAdapter, isServer, isBrowser } from './platform-adapters.js';
2
+
3
+ // Import type definitions
4
+ /**
5
+ * @typedef {import('./types.js').Event} Event
6
+ * @typedef {import('./types.js').PageViewEvent} PageViewEvent
7
+ * @typedef {import('./types.js').CustomEvent} CustomEvent
8
+ * @typedef {import('./types.js').ServerRequestEvent} ServerRequestEvent
9
+ * @typedef {import('./types.js').AnalyticsBatch} AnalyticsBatch
10
+ */
11
+
12
+ export class AnalyticsTracker {
13
+ /**
14
+ * @param {Object} config - Tracker configuration
15
+ * @param {string} config.backendUrl - URL to send analytics data
16
+ * @param {string} [config.appId] - Application ID for tracking (can also be set via WEBTICKS_APP_ID env variable)
17
+ */
18
+ constructor(config) {
19
+ this.config = config || { backendUrl: "/api/track" };
20
+
21
+ // Get appId from config or environment variable
22
+ this.appId = this.config.appId || this.getAppIdFromEnv();
23
+
24
+ /** @type {Event[]} */
25
+ this.eventQueue = [];
26
+ this.lastPath = "";
27
+ this.batchSendInterval = 10000;
28
+ this.sendTimer = null;
29
+ /** @type {string|null} */
30
+ this.userId = null;
31
+ /** @type {string|null} */
32
+ this.sessionId = null; // Session ID stored in memory
33
+ this.adapter = getPlatformAdapter();
34
+
35
+ // Bind methods
36
+ this.checkPageChange = this.checkPageChange.bind(this);
37
+ this.sendQueue = this.sendQueue.bind(this);
38
+ this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
39
+ this.handlePageHide = this.handlePageHide.bind(this);
40
+
41
+ this.initializeUser();
42
+ this.initializeSession();
43
+
44
+ console.log(`AnalyticsTracker initialized in ${isServer() ? 'Node.js' : 'Browser'} environment.`);
45
+ }
46
+
47
+ /**
48
+ * Get app ID from environment variable
49
+ * Works in both browser (import.meta.env) and Node.js (process.env)
50
+ */
51
+ getAppIdFromEnv() {
52
+ // Browser environment (Vite, etc.)
53
+ if (typeof import.meta !== 'undefined' && import.meta.env) {
54
+ return import.meta.env.VITE_WEBTICKS_APP_ID || import.meta.env.WEBTICKS_APP_ID;
55
+ }
56
+ // Node.js environment
57
+ if (typeof process !== 'undefined' && process.env) {
58
+ return process.env.WEBTICKS_APP_ID;
59
+ }
60
+ return null;
61
+ }
62
+
63
+ initializeUser() {
64
+ // Skip if adapter is not available (will be initialized later in server environments)
65
+ if (!this.adapter) {
66
+ return;
67
+ }
68
+
69
+ if (!this.userId) {
70
+ this.userId = this.adapter.getUserId();
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Initialize session ID
76
+ * Session is stored in memory and will be destroyed when the page/instance is closed
77
+ */
78
+ initializeSession() {
79
+ // Generate new session ID using crypto (available in both browser and Node.js)
80
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
81
+ this.sessionId = crypto.randomUUID();
82
+ console.log(`Session initialized: ${this.sessionId}`);
83
+ } else {
84
+ // Fallback for older environments
85
+ this.sessionId = this.generateFallbackId();
86
+ console.warn('crypto.randomUUID not available, using fallback ID generation');
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Generate fallback ID if crypto.randomUUID is not available
92
+ */
93
+ generateFallbackId() {
94
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
95
+ const r = Math.random() * 16 | 0;
96
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
97
+ return v.toString(16);
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Automatically track page views (browser) or HTTP requests (server)
103
+ * Works in both environments!
104
+ */
105
+ autoTrackPageViews() {
106
+ if (isServer()) {
107
+ console.log("Setting up automatic server-side tracking...");
108
+ } else {
109
+ console.log("Setting up automatic page view tracking...");
110
+ }
111
+
112
+ // Use adapter to setup platform-specific tracking
113
+ this.adapter.setupAutoTracking(this);
114
+
115
+ // Start the batch send timer
116
+ this.sendTimer = setInterval(this.sendQueue, this.batchSendInterval);
117
+ }
118
+
119
+ /**
120
+ * Check for page changes (browser-only)
121
+ */
122
+ checkPageChange() {
123
+ if (isServer()) return;
124
+
125
+ const currentPath = this.adapter.getCurrentPath();
126
+ if (currentPath !== this.lastPath) {
127
+ console.log(`Page change detected: ${this.lastPath} -> ${currentPath}`);
128
+ this.lastPath = currentPath;
129
+ this.trackPageView(currentPath);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Handle visibility changes (browser-only)
135
+ */
136
+ handleVisibilityChange() {
137
+ if (isServer()) return;
138
+
139
+ if (document.hidden) {
140
+ this.trackEvent('visibility_change', { visible: false });
141
+ } else {
142
+ this.trackEvent('visibility_change', { visible: true });
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Handle page hide event (browser-only)
148
+ */
149
+ handlePageHide() {
150
+ console.log("Page hidden, attempting final batch send.");
151
+ this.sendQueue();
152
+ }
153
+
154
+ /**
155
+ * Track a page view
156
+ * @param {string} path - The path/URL to track
157
+ * @returns {PageViewEvent} The created event
158
+ */
159
+ trackPageView(path) {
160
+ /** @type {PageViewEvent} */
161
+ const event = {
162
+ requestId: crypto.randomUUID ? crypto.randomUUID() : this.generateFallbackId(),
163
+ type: 'pageview',
164
+ path: path,
165
+ timestamp: new Date().toISOString()
166
+ };
167
+
168
+ this.eventQueue.push(event);
169
+ return event;
170
+ }
171
+
172
+ /**
173
+ * Track a custom event
174
+ * @param {string} eventName - Name of the event
175
+ * @param {Object} [details={}] - Event details/metadata
176
+ * @returns {CustomEvent} The created event
177
+ */
178
+ trackEvent(eventName, details = {}) {
179
+ /** @type {CustomEvent} */
180
+ const event = {
181
+ requestId: crypto.randomUUID ? crypto.randomUUID() : this.generateFallbackId(),
182
+ type: 'custom',
183
+ name: eventName,
184
+ details: details,
185
+ path: isBrowser() ? window.location.href : null,
186
+ timestamp: new Date().toISOString()
187
+ };
188
+
189
+ this.eventQueue.push(event);
190
+ return event;
191
+ }
192
+
193
+ /**
194
+ * Track a server-side HTTP request (server-only)
195
+ * @param {Object} requestData - Request information
196
+ * @param {string} requestData.method - HTTP method (GET, POST, etc.)
197
+ * @param {string} requestData.path - Request path
198
+ * @param {Object} [requestData.query] - Query parameters
199
+ * @param {Object} [requestData.headers] - Request headers
200
+ * @returns {ServerRequestEvent} The created event
201
+ */
202
+ trackServerRequest(requestData) {
203
+ /** @type {ServerRequestEvent} */
204
+ const event = {
205
+ requestId: crypto.randomUUID ? crypto.randomUUID() : this.generateFallbackId(),
206
+ type: 'server_request',
207
+ method: requestData.method,
208
+ path: requestData.path,
209
+ query: requestData.query,
210
+ headers: requestData.headers,
211
+ timestamp: new Date().toISOString()
212
+ };
213
+
214
+ this.eventQueue.push(event);
215
+ return event;
216
+ }
217
+
218
+ /**
219
+ * Send queued events to the backend
220
+ */
221
+ async sendQueue() {
222
+ if (this.eventQueue.length === 0) {
223
+ return;
224
+ }
225
+
226
+ const eventsToSend = [...this.eventQueue];
227
+
228
+ try {
229
+ const response = await this.adapter.sendRequest(this.config.backendUrl, {
230
+ uid: this.userId,
231
+ sessionId: this.sessionId,
232
+ events: eventsToSend,
233
+ datetime: new Date().toISOString()
234
+ }, this.appId);
235
+
236
+ if (response.ok) {
237
+ // Clear queue on success
238
+ this.eventQueue = [];
239
+ } else {
240
+ console.error(`Failed to send analytics batch: ${response.status}`);
241
+ // Keep events in queue to retry
242
+ this.eventQueue = [...eventsToSend];
243
+ }
244
+ } catch (err) {
245
+ console.error("Failed to send analytics batch:", err);
246
+ // Keep events in queue to retry
247
+ this.eventQueue = [...eventsToSend];
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Cleans up listeners and timers
253
+ */
254
+ destroy() {
255
+ console.log("Destroying tracker...");
256
+
257
+ // Stop the batch sender
258
+ if (this.sendTimer) {
259
+ clearInterval(this.sendTimer);
260
+ }
261
+
262
+ // Use adapter to cleanup platform-specific tracking
263
+ this.adapter.cleanupAutoTracking(this);
264
+ }
265
+ }
package/types.js ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Type definitions for WebTicks Analytics Events
3
+ * Using JSDoc for type safety in JavaScript
4
+ */
5
+
6
+ /**
7
+ * Base Event interface - common fields for all events
8
+ * @typedef {Object} BaseEvent
9
+ * @property {string} requestId - Unique identifier for this specific request
10
+ * @property {string} type - Type of event ('pageview' | 'custom' | 'server_request')
11
+ * @property {string} timestamp - ISO timestamp when the event was created
12
+ */
13
+
14
+ /**
15
+ * Page View Event - tracks page navigation
16
+ * @typedef {Object} PageViewEvent
17
+ * @property {string} requestId - Unique identifier for this specific request
18
+ * @property {'pageview'} type - Event type identifier
19
+ * @property {string} path - The URL/path that was viewed
20
+ * @property {string} timestamp - ISO timestamp when the event was created
21
+ */
22
+
23
+ /**
24
+ * Custom Event - tracks custom user interactions
25
+ * @typedef {Object} CustomEvent
26
+ * @property {string} requestId - Unique identifier for this specific request
27
+ * @property {'custom'} type - Event type identifier
28
+ * @property {string} name - Name of the custom event
29
+ * @property {Object} details - Additional event metadata
30
+ * @property {string|null} path - Current page URL (browser only)
31
+ * @property {string} timestamp - ISO timestamp when the event was created
32
+ */
33
+
34
+ /**
35
+ * Server Request Event - tracks server-side HTTP requests
36
+ * @typedef {Object} ServerRequestEvent
37
+ * @property {string} requestId - Unique identifier for this specific request
38
+ * @property {'server_request'} type - Event type identifier
39
+ * @property {string} method - HTTP method (GET, POST, etc.)
40
+ * @property {string} path - Request path
41
+ * @property {Object} [query] - Query parameters
42
+ * @property {Object} [headers] - Request headers
43
+ * @property {string} timestamp - ISO timestamp when the event was created
44
+ */
45
+
46
+ /**
47
+ * Union type for all possible events
48
+ * @typedef {PageViewEvent | CustomEvent | ServerRequestEvent} Event
49
+ */
50
+
51
+ /**
52
+ * Analytics batch payload sent to backend
53
+ * @typedef {Object} AnalyticsBatch
54
+ * @property {string} uid - User ID
55
+ * @property {string} sessionId - Session ID
56
+ * @property {Event[]} events - Array of events to track
57
+ * @property {string} datetime - ISO timestamp when the batch was sent
58
+ */
59
+
60
+ // Export types for use in other modules
61
+ export { };