Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions app/analytics-worker/analyticsWorker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { Worker, Queue } from "bullmq";
import { redis } from "@glimpse/db/redis";
import { prisma } from "@glimpse/db/client";


/**
* What to precompute for analytics:
*
* AnalyticsSession
* DailyEventAggregate
* DailyPageAnalytics
* DailyUserAnalytics
* AnalyticsCheckpoint
*/

const redisPort = process.env.REDIS_PORT
? Number(process.env.REDIS_PORT)
: 6379;
Expand All @@ -23,7 +33,18 @@ const analyticsWorker = new Worker(
switch (job.name) {
case "analytics-job":
const { data } = job;
// Process the analytics data

// AnalyticsSession computation

const analyticsSession = {
projectId: data.projectId,
sessionId: data.sessionId,


createdAt : new Date(),
updatedAt : new Date(),
}

Comment on lines +37 to +47

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The analyticsSession object is created but never used. This appears to be incomplete placeholder code with empty lines (42-43). Either complete the implementation or remove this unused code.

Suggested change
// AnalyticsSession computation
const analyticsSession = {
projectId: data.projectId,
sessionId: data.sessionId,
createdAt : new Date(),
updatedAt : new Date(),
}
// TODO: Implement AnalyticsSession computation and persistence using `data`.

Copilot uses AI. Check for mistakes.
break;
Comment on lines 34 to 48

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete implementation leaves analyticsSession object constructed but never used - no database insert or return statement. The break statement exits the switch without processing.

Suggested change
case "analytics-job":
const { data } = job;
// Process the analytics data
// AnalyticsSession computation
const analyticsSession = {
projectId: data.projectId,
sessionId: data.sessionId,
createdAt : new Date(),
updatedAt : new Date(),
}
break;
case "analytics-job":
const { data } = job;
// AnalyticsSession computation
const analyticsSession = {
projectId: data.projectId,
sessionId: data.sessionId,
createdAt: new Date(),
updatedAt: new Date(),
};
// Store in database
await prisma.analyticsSession.upsert({
where: { sessionId: analyticsSession.sessionId },
update: { updatedAt: analyticsSession.updatedAt },
create: analyticsSession,
});
break;
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/analytics-worker/analyticsWorker.ts
Line: 34:48

Comment:
Incomplete implementation leaves `analyticsSession` object constructed but never used - no database insert or return statement. The `break` statement exits the switch without processing.

```suggestion
            case "analytics-job":
                const { data } = job;

                // AnalyticsSession computation
                const analyticsSession = {
                    projectId: data.projectId,
                    sessionId: data.sessionId,
                    createdAt: new Date(),
                    updatedAt: new Date(), 
                };

                // Store in database
                await prisma.analyticsSession.upsert({
                    where: { sessionId: analyticsSession.sessionId },
                    update: { updatedAt: analyticsSession.updatedAt },
                    create: analyticsSession,
                });

                break;
```

How can I resolve this? If you propose a fix, please make it concise.

default:
console.warn(`Unknown job type: ${job.name}`);
Expand Down
87 changes: 87 additions & 0 deletions packages/analytics-web/core/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* SDK Configuration
* Supports both script tag attributes and programmatic initialization
*/

let config = {
projectId: undefined,
endpoint: 'http://localhost:3000/event',
autoTrack: true,
debug: false,
trackPageViews: true,
trackWebVitals: true,
trackEngagement: true,
trackErrors: true,
sessionTimeout: 30 * 60 * 1000, // 30 minutes
};

let initialized = false;

/**
* Check if running in browser
*/
export function isBrowser() {
return typeof window !== 'undefined' && typeof document !== 'undefined';
}

/**
* Get current config
*/
export function getConfig() {
return { ...config };
}

/**
* Initialize the SDK programmatically
* Use this for React, Vue, Next.js, etc.
*/
export function init(options = {}) {
if (!isBrowser()) {
console.warn('GlimpseTracker: Cannot initialize on server. Use in useEffect/onMounted.');
return false;
}

if (initialized && !options.force) {
if (config.debug) console.info('GlimpseTracker: Already initialized');
return true;
}

config = {
...config,
...options,
};

if (!config.projectId) {
console.warn('GlimpseTracker: projectId is required');
return false;
}

initialized = true;

// Update global tracker
if (window.GlimpseTracker) {
window.GlimpseTracker.projectId = config.projectId;
window.GlimpseTracker.endpoint = config.endpoint;
window.GlimpseTracker.debug = config.debug;
}

if (config.debug) {
console.info('GlimpseTracker: Initialized', config);
}

return true;
}

/**
* Check if SDK is initialized
*/
export function isInitialized() {
return initialized;
}

/**
* Reset initialization (for testing)
*/
export function resetInit() {

@cubic-dev-ai cubic-dev-ai Bot Feb 2, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: resetInit() doesn't reset config to defaults, only the initialized flag. Since init() mutates the module-level config, this causes test pollution - configuration from previous tests will persist after reset. The function should also restore config to its default values.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/analytics-web/core/config.js, line 85:

<comment>`resetInit()` doesn't reset `config` to defaults, only the `initialized` flag. Since `init()` mutates the module-level `config`, this causes test pollution - configuration from previous tests will persist after reset. The function should also restore `config` to its default values.</comment>

<file context>
@@ -0,0 +1,87 @@
+/**
+ * Reset initialization (for testing)
+ */
+export function resetInit() {
+    initialized = false;
+}
</file context>
Fix with Cubic

initialized = false;
}
63 changes: 49 additions & 14 deletions packages/analytics-web/core/event.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,78 @@
import { getAnonymousId, getUserId, getTraits } from './identity.js';

// Track previous path within the session
let prevPath = sessionStorage.getItem('analytics_prev_path') || undefined;
let prevPath = sessionStorage.getItem('glimpse_prev_path') || undefined;

export function createEvent(projectId, sessionId, name, properties) {
const currentPath = location.pathname + location.search + location.hash;
/**
* Create a fully-formed event object matching the schema
*/
export function createEvent(projectId, sessionId, name, properties = {}) {
const currentPath = location.pathname + location.search;

const evt = {
projectId,
event: name,
timestamp: Date.now(),

// Identity
sessionId,
anonymousId: getAnonymousId(),
userId: getUserId(),
traits: getTraits(),

// Event data
properties: properties || {},

// Context
context: {
// Page info
url: window.location.href,
referrer: document.referrer || undefined,
path: currentPath,
hash: location.hash || undefined,
title: document.title || undefined,
referrer: document.referrer || undefined,
previousPath: prevPath,

viewport: `${window.innerWidth}x${window.innerHeight}`,

// Device & viewport
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
screen: {
width: window.screen.width,
height: window.screen.height,
colorDepth: window.screen.colorDepth
colorDepth: window.screen.colorDepth,
pixelRatio: window.devicePixelRatio || 1
},


// Browser info
userAgent: navigator.userAgent,
language: navigator.language,
languages: navigator.languages ? [...navigator.languages] : [navigator.language],
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,

connection: navigator.connection && {
cookiesEnabled: navigator.cookieEnabled,

// Connection info
connection: navigator.connection ? {
effectiveType: navigator.connection.effectiveType,
saveData: navigator.connection.saveData,
},
downlink: navigator.connection.downlink,
rtt: navigator.connection.rtt,
saveData: navigator.connection.saveData
} : undefined,

// Touch capability
touchPoints: navigator.maxTouchPoints || 0,

// Platform hints
platform: navigator.userAgentData ? {
mobile: navigator.userAgentData.mobile,
platform: navigator.userAgentData.platform
} : undefined
Comment on lines +67 to +70

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The navigator.userAgentData API is part of the User-Agent Client Hints specification which has limited browser support. Consider documenting this or adding a feature detection note for developers using older browsers.

Copilot uses AI. Check for mistakes.
}
};

prevPath = currentPath;
sessionStorage.setItem('analytics_prev_path', currentPath);
sessionStorage.setItem('glimpse_prev_path', currentPath);

return evt;
}

69 changes: 69 additions & 0 deletions packages/analytics-web/core/identity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const ANON_ID_KEY = 'glimpse_anonymous_id';
const USER_ID_KEY = 'glimpse_user_id';
const TRAITS_KEY = 'glimpse_traits';

/**
* Get or create a persistent anonymous ID
*/
export function getAnonymousId() {
let anonId = localStorage.getItem(ANON_ID_KEY);

@cubic-dev-ai cubic-dev-ai Bot Feb 2, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Missing error handling for localStorage access. This function accesses localStorage.getItem() and localStorage.setItem() without try-catch, but getTraits() in the same file wraps similar operations in try-catch. localStorage can throw QuotaExceededError or SecurityError in certain browser contexts (private browsing, disabled storage, quota exceeded). Consider wrapping in try-catch for consistency and resilience.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/analytics-web/core/identity.js, line 9:

<comment>Missing error handling for localStorage access. This function accesses `localStorage.getItem()` and `localStorage.setItem()` without try-catch, but `getTraits()` in the same file wraps similar operations in try-catch. localStorage can throw `QuotaExceededError` or `SecurityError` in certain browser contexts (private browsing, disabled storage, quota exceeded). Consider wrapping in try-catch for consistency and resilience.</comment>

<file context>
@@ -0,0 +1,69 @@
+ * Get or create a persistent anonymous ID
+ */
+export function getAnonymousId() {
+    let anonId = localStorage.getItem(ANON_ID_KEY);
+    if (!anonId) {
+        anonId = `anon_${crypto.randomUUID ? crypto.randomUUID() : generateUUID()}`;
</file context>
Fix with Cubic

if (!anonId) {
anonId = `anon_${crypto.randomUUID ? crypto.randomUUID() : generateUUID()}`;

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ternary operator checks if crypto.randomUUID exists but doesn't invoke it. It should be 'crypto.randomUUID()' in both the condition check and the usage. Currently this works due to JavaScript's truthy evaluation, but explicitly checking the function's availability would be clearer.

Suggested change
anonId = `anon_${crypto.randomUUID ? crypto.randomUUID() : generateUUID()}`;
anonId = `anon_${typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : generateUUID()}`;

Copilot uses AI. Check for mistakes.
localStorage.setItem(ANON_ID_KEY, anonId);
}
return anonId;
}

/**
* Get the current user ID (if identified)
*/
export function getUserId() {
return localStorage.getItem(USER_ID_KEY) || undefined;
}

/**
* Get stored user traits
*/
export function getTraits() {
try {
const stored = localStorage.getItem(TRAITS_KEY);
return stored ? JSON.parse(stored) : {};
} catch {
return {};
}
}

/**
* Set user identity and traits
*/
export function setIdentity(userId, traits = {}) {
if (userId) {
localStorage.setItem(USER_ID_KEY, userId);
}

const existingTraits = getTraits();
const mergedTraits = { ...existingTraits, ...traits };
localStorage.setItem(TRAITS_KEY, JSON.stringify(mergedTraits));

return { userId, traits: mergedTraits };
}

/**
* Reset identity (logout)
*/
export function resetIdentity() {
localStorage.removeItem(USER_ID_KEY);
localStorage.removeItem(TRAITS_KEY);
// Keep anonymous ID - regenerate on next page load if needed
}

/**
* Fallback UUID generator for older browsers
*/
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
69 changes: 66 additions & 3 deletions packages/analytics-web/core/session.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,72 @@
const SESSION_ID_KEY = 'glimpse_session_id';
const SESSION_START_KEY = 'glimpse_session_start';
const SESSION_LAST_ACTIVE_KEY = 'glimpse_session_last_active';
const SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes

/**
* Get or create session ID
* Sessions expire after 30 minutes of inactivity
*/
export function getSessionId() {
let sid = sessionStorage.getItem('analytics_session_id');
const now = Date.now();
let sid = sessionStorage.getItem(SESSION_ID_KEY);
const lastActive = parseInt(sessionStorage.getItem(SESSION_LAST_ACTIVE_KEY) || '0', 10);

// Check if session expired
if (sid && lastActive && (now - lastActive > SESSION_TIMEOUT)) {
// Session expired, create new one
sid = null;
}

if (!sid) {
sid = `sess_${Math.random().toString(36).substring(2, 15)}`;
sessionStorage.setItem('analytics_session_id', sid);
sid = `sess_${crypto.randomUUID ? crypto.randomUUID().slice(0, 12) : Math.random().toString(36).substring(2, 15)}`;

Copilot AI Feb 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using crypto.randomUUID().slice(0, 12) reduces the UUID from 36 characters to 12, significantly decreasing entropy and increasing collision risk. Consider using the full UUID or a cryptographically secure alternative that maintains sufficient entropy.

Suggested change
sid = `sess_${crypto.randomUUID ? crypto.randomUUID().slice(0, 12) : Math.random().toString(36).substring(2, 15)}`;
sid = `sess_${crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2, 15)}`;

Copilot uses AI. Check for mistakes.
sessionStorage.setItem(SESSION_ID_KEY, sid);
sessionStorage.setItem(SESSION_START_KEY, now.toString());
}

// Update last active time
sessionStorage.setItem(SESSION_LAST_ACTIVE_KEY, now.toString());

return sid;
}

/**
* Check if this is a new session (first page view or after timeout)
*/
export function isNewSession() {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
const sid = sessionStorage.getItem(SESSION_ID_KEY);
if (!sid) return true;

const lastActive = parseInt(sessionStorage.getItem(SESSION_LAST_ACTIVE_KEY) || '0', 10);
return lastActive && (Date.now() - lastActive > SESSION_TIMEOUT);
}
Comment on lines +36 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isNewSession() has incorrect logic - it returns true only before getSessionId() is first called, but if called after, it always returns false even for genuinely new sessions (e.g., after 30min timeout). This breaks session start tracking.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/analytics-web/core/session.js
Line: 36:38

Comment:
`isNewSession()` has incorrect logic - it returns true only before `getSessionId()` is first called, but if called after, it always returns false even for genuinely new sessions (e.g., after 30min timeout). This breaks session start tracking.

How can I resolve this? If you propose a fix, please make it concise.


/**
* Get session start timestamp
*/
export function getSessionStart() {
return parseInt(sessionStorage.getItem(SESSION_START_KEY) || Date.now().toString(), 10);
}

/**
* Get session duration in milliseconds
*/
export function getSessionDuration() {
const start = getSessionStart();
return Date.now() - start;
}

/**
* Track page view count in session
*/
const PAGE_VIEW_COUNT_KEY = 'glimpse_page_view_count';

export function incrementPageViewCount() {
const count = getPageViewCount() + 1;
sessionStorage.setItem(PAGE_VIEW_COUNT_KEY, count.toString());
return count;
}

export function getPageViewCount() {
return parseInt(sessionStorage.getItem(PAGE_VIEW_COUNT_KEY) || '0', 10);
}
Loading