From 4c326d2ad43090c1ed2a9b6c2df9fd909d91429e Mon Sep 17 00:00:00 2001 From: Taly Date: Wed, 28 Jan 2026 19:41:09 +0300 Subject: [PATCH 01/44] Add demo mode with API mocks and enablement logic Introduces demo mode support by adding mock API responses for events, projects, user, and workspaces. Refactors API modules to use a withDemoMock utility for conditional mock injection. Updates EventsList and store modules for improved debug logging and error handling. Demo mode can be enabled automatically or via browser console for local development. --- .nvmrc | 2 +- src/api/events/fetchChartData.mock.ts | 37 ++++ .../events/fetchDailyEventsPortion.mock.ts | 174 +++++++++++++++++ src/api/events/getEvent.mock.ts | 89 +++++++++ src/api/events/getRepetitionsPortion.mock.ts | 156 +++++++++++++++ src/api/events/index.ts | 60 ++++-- src/api/events/toggleEventMark.mock.ts | 6 + src/api/events/visitEvent.mock.ts | 6 + src/api/projects/fetchChartData.mock.js | 47 +++++ .../fetchProjectReleaseDetails.mock.ts | 74 +++++++ src/api/projects/fetchProjectReleases.mock.ts | 66 +++++++ src/api/projects/getProjects.mock.ts | 8 + src/api/projects/index.js | 55 +++++- src/api/user/demoUser.mock.ts | 13 ++ src/api/user/fetchBankCards.mock.ts | 25 +++ src/api/user/fetchCurrentUser.mock.ts | 27 +++ src/api/user/index.js | 41 +++- src/api/user/login.mock.ts | 14 ++ src/api/user/signUp.mock.ts | 11 ++ .../getAllWorkspacesWithProjects.mock.ts | 77 ++++++++ src/api/workspaces/getWorkspaces.mock.ts | 80 ++++++++ src/api/workspaces/index.ts | 21 +- src/components/project/EventsList.vue | 26 ++- src/main.ts | 22 +++ src/store/modules/events/index.ts | 38 +++- src/utils/withDemoMock.ts | 182 ++++++++++++++++++ 26 files changed, 1317 insertions(+), 40 deletions(-) create mode 100644 src/api/events/fetchChartData.mock.ts create mode 100644 src/api/events/fetchDailyEventsPortion.mock.ts create mode 100644 src/api/events/getEvent.mock.ts create mode 100644 src/api/events/getRepetitionsPortion.mock.ts create mode 100644 src/api/events/toggleEventMark.mock.ts create mode 100644 src/api/events/visitEvent.mock.ts create mode 100644 src/api/projects/fetchChartData.mock.js create mode 100644 src/api/projects/fetchProjectReleaseDetails.mock.ts create mode 100644 src/api/projects/fetchProjectReleases.mock.ts create mode 100644 src/api/projects/getProjects.mock.ts create mode 100644 src/api/user/demoUser.mock.ts create mode 100644 src/api/user/fetchBankCards.mock.ts create mode 100644 src/api/user/fetchCurrentUser.mock.ts create mode 100644 src/api/user/login.mock.ts create mode 100644 src/api/user/signUp.mock.ts create mode 100644 src/api/workspaces/getAllWorkspacesWithProjects.mock.ts create mode 100644 src/api/workspaces/getWorkspaces.mock.ts create mode 100644 src/utils/withDemoMock.ts diff --git a/.nvmrc b/.nvmrc index 8ef0a5258..a682cfb97 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v24.11.1 +v25 diff --git a/src/api/events/fetchChartData.mock.ts b/src/api/events/fetchChartData.mock.ts new file mode 100644 index 000000000..470aec8a9 --- /dev/null +++ b/src/api/events/fetchChartData.mock.ts @@ -0,0 +1,37 @@ +import type { ChartLine } from '@/types/chart'; + +/** + * Mock data for fetchChartData + */ +const mockFetchChartData: ChartLine[] = [ + { + date: '2026-01-28', + count: 42, + }, + { + date: '2026-01-27', + count: 35, + }, + { + date: '2026-01-26', + count: 28, + }, + { + date: '2026-01-25', + count: 31, + }, + { + date: '2026-01-24', + count: 25, + }, + { + date: '2026-01-23', + count: 18, + }, + { + date: '2026-01-22', + count: 22, + }, +]; + +export default mockFetchChartData; diff --git a/src/api/events/fetchDailyEventsPortion.mock.ts b/src/api/events/fetchDailyEventsPortion.mock.ts new file mode 100644 index 000000000..a0245d830 --- /dev/null +++ b/src/api/events/fetchDailyEventsPortion.mock.ts @@ -0,0 +1,174 @@ +import type { DailyEventsPortion, HawkEvent } from '@/types/events'; +import { DEMO_USER } from '@/api/user/demoUser.mock'; + +/** + * Create a complete mock event with all fields + */ +const createDemoEvent = ( + id: string, + originalId: string, + title: string, + type: string, + groupHash: string, + totalCount: number, + usersAffected: number +): HawkEvent => ({ + id, + groupHash, + totalCount, + visitedBy: [DEMO_USER], + marks: { + resolved: false, + starred: false, + ignored: false, + }, + payload: { + title: title, + type: type, + backtrace: [ + { + file: 'src/store/user.ts', + line: 42, + column: 15, + function: 'getUserProfile', + arguments: ['userId'], + sourceCode: [ + { line: 40, content: 'export const getUserProfile = (userId) => {' }, + { line: 41, content: ' const user = store.getters.user;' }, + { line: 42, content: ' return user.profile.settings;' }, + { line: 43, content: '};' }, + ], + }, + { + file: 'src/components/UserPanel.vue', + line: 28, + column: 8, + function: 'mounted', + arguments: [], + sourceCode: [ + { line: 26, content: 'mounted() {' }, + { line: 27, content: ' this.profile = getUserProfile(this.userId);' }, + { line: 28, content: ' console.log(this.profile.email);' }, + { line: 29, content: '}' }, + ], + }, + ], + get: { + userId: '507f1f77bcf86cd799439011', + format: 'json', + }, + post: { + action: 'getUserData', + timestamp: Date.now(), + }, + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', + 'Accept-Language': 'en-US,en;q=0.9', + 'X-Requested-With': 'XMLHttpRequest', + }, + release: 'v1.0.0', + user: { + id: 'user-demo-001', + } as any, + context: { + browser: 'Chrome 120.0.0', + os: 'macOS 14.2.1', + screen: '1920x1080', + timezone: 'UTC+2', + language: 'en-US', + url: 'http://localhost:8080/project/6215743cf3ff6b80215cb183', + referrer: 'http://localhost:8080/workspaces', + }, + addons: {} as any, + }, + catcherType: 'client/javascript', + repetitions: [], + usersAffected, + assignee: undefined as any, + timestamp: Date.now(), + originalTimestamp: Date.now() - 86400000, + originalEventId: originalId, +}); + +/** + * Helper to create fresh mock data each time (prevents mutation issues) + */ +const createMockDailyEventsPortion = (): DailyEventsPortion => ({ + nextCursor: null, + dailyEvents: [ + { + id: '6789abcd1234567890123456', + groupingTimestamp: Math.floor(Date.now() / 86400000) * 86400000, + count: 42, + affectedUsers: 8, + event: createDemoEvent( + '507f1f77bcf86cd799439011', + '507f1f77bcf86cd799439010', + 'TypeError: Cannot read property \'user\' of undefined', + 'TypeError', + 'hash-507f1f77bcf86cd799439011', + 42, + 8 + ), + }, + { + id: '6789abcd1234567890123457', + groupingTimestamp: Math.floor(Date.now() / 86400000) * 86400000, + count: 18, + affectedUsers: 5, + event: createDemoEvent( + '507f191e810c19729de860ea', + '507f191e810c19729de860e9', + 'ReferenceError: apiKey is not defined', + 'ReferenceError', + 'hash-507f191e810c19729de860ea', + 18, + 5 + ), + }, + { + id: '6789abcd1234567890123458', + groupingTimestamp: Math.floor(Date.now() / 86400000) * 86400000, + count: 7, + affectedUsers: 3, + event: createDemoEvent( + '507f1f77bcf86cd799439012', + '507f1f77bcf86cd799439013', + 'Network Error: Failed to fetch user data from API', + 'NetworkError', + 'hash-507f1f77bcf86cd799439012', + 7, + 3 + ), + }, + { + id: '6789abcd1234567890123459', + groupingTimestamp: Math.floor(Date.now() / 86400000) * 86400000, + count: 12, + affectedUsers: 4, + event: createDemoEvent( + '507f1f77bcf86cd799439014', + '507f1f77bcf86cd799439015', + 'SyntaxError: Unexpected token < in JSON at position 0', + 'SyntaxError', + 'hash-507f1f77bcf86cd799439014', + 12, + 4 + ), + }, + ], +}); + +// Validate structure (create instance just for validation logs) +const sampleMock = createMockDailyEventsPortion(); +console.log('[🔍 DEBUG] fetchDailyEventsPortion mock export - dailyEvents structure:'); +sampleMock.dailyEvents.forEach((de, i) => { + if (de.event?.id) { + console.log(` ✅ Event ${i}: eventId=${de.event.id} (${de.event.payload?.title})`); + } else { + console.error(` ❌ Event ${i}: MISSING event.id!`, de); + } +}); + +// Export function that creates fresh mock each time +export default createMockDailyEventsPortion; diff --git a/src/api/events/getEvent.mock.ts b/src/api/events/getEvent.mock.ts new file mode 100644 index 000000000..fe5588d6c --- /dev/null +++ b/src/api/events/getEvent.mock.ts @@ -0,0 +1,89 @@ +import type { HawkEvent } from '@/types/events'; +import { DEMO_USER } from '@/api/user/demoUser.mock'; + +/** + * Mock data for getEvent - detailed event information + */ +const mockGetEvent: HawkEvent = { + id: '507f1f77bcf86cd799439011', + originalEventId: '507f1f77bcf86cd799439010', + groupHash: 'hash-507f1f77bcf86cd799439011', + totalCount: 42, + usersAffected: 8, + timestamp: Date.now(), + originalTimestamp: Date.now() - 604800000, // 1 week ago + payload: { + title: 'TypeError: Cannot read property \'user\' of undefined', + type: 'TypeError', + backtrace: [ + { + file: 'src/store/user.ts', + line: 42, + column: 15, + function: 'getUserProfile', + arguments: ['userId'], + sourceCode: [ + { line: 40, content: 'export const getUserProfile = (userId) => {' }, + { line: 41, content: ' const user = store.getters.user;' }, + { line: 42, content: ' return user.profile.settings;' }, + { line: 43, content: '};' }, + ], + }, + { + file: 'src/components/UserPanel.vue', + line: 28, + column: 8, + function: 'onMounted', + arguments: [], + sourceCode: [ + { line: 26, content: 'onMounted(() => {' }, + { line: 27, content: ' profile.value = getUserProfile(userId);' }, + { line: 28, content: ' console.log(profile.value.email);' }, + { line: 29, content: '});' }, + ], + }, + ], + get: { + userId: '507f1f77bcf86cd799439011', + profileId: '507f191e810c19729de860ea', + format: 'json', + }, + post: { + action: 'loadUserProfile', + timestamp: Date.now(), + requestId: 'req-507f1f77bcf86cd799439011', + }, + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + 'Accept-Language': 'en-US,en;q=0.9', + 'X-Requested-With': 'XMLHttpRequest', + 'X-Request-ID': 'req-507f1f77bcf86cd799439011', + 'Referer': 'http://localhost:8080/project/6215743cf3ff6b80215cb183', + }, + release: 'v1.2.3', + user: { + id: 'user-demo-001', + } as any, + context: { + browser: 'Chrome 120.0.0', + os: 'macOS 14.2.1 (Sonoma)', + screen: '1920x1080', + timezone: 'UTC+2', + language: 'en-US', + url: 'http://localhost:8080/project/6215743cf3ff6b80215cb183/events/507f1f77bcf86cd799439011', + referrer: 'http://localhost:8080/project/6215743cf3ff6b80215cb183', + }, + addons: {} as any, + }, + catcherType: 'client/javascript', + repetitions: [], + visitedBy: [DEMO_USER], + marks: { + resolved: false, + starred: true, + ignored: false, + }, + assignee: undefined as any, +}; + +export default mockGetEvent; diff --git a/src/api/events/getRepetitionsPortion.mock.ts b/src/api/events/getRepetitionsPortion.mock.ts new file mode 100644 index 000000000..95e980427 --- /dev/null +++ b/src/api/events/getRepetitionsPortion.mock.ts @@ -0,0 +1,156 @@ +import type { HawkEvent } from '@/types/events'; +import { DEMO_USER } from '@/api/user/demoUser.mock'; + +/** + * Helper to create detailed mock repetition events + */ +const createRepetitionEvent = ( + id: string, + originalId: string, + title: string, + type: string, + groupHash: string, + timestamp: number, + count: number, + browser: string, + os: string +): HawkEvent => ({ + id, + groupHash, + totalCount: count, + visitedBy: [DEMO_USER], + marks: { + resolved: false, + starred: false, + ignored: false, + }, + payload: { + title: title, + type: type, + backtrace: [ + { + file: 'src/store/modules/user.ts', + line: 42, + column: 15, + function: 'getUserProfile', + arguments: ['userId'], + sourceCode: [ + { line: 40, content: 'export const getUserProfile = (userId) => {' }, + { line: 41, content: ' const user = state.users[userId];' }, + { line: 42, content: ' return user.profile.settings;' }, + { line: 43, content: '};' }, + ], + }, + { + file: 'src/components/UserPanel.vue', + line: 28, + column: 8, + function: 'mounted', + arguments: [], + sourceCode: [ + { line: 26, content: 'mounted() {' }, + { line: 27, content: ' this.profile = getUserProfile(this.userId);' }, + { line: 28, content: ' console.log(this.profile.email);' }, + { line: 29, content: '}' }, + ], + }, + ], + get: { + userId: 'user-123', + profileId: 'profile-456', + }, + post: { + action: 'loadUserProfile', + timestamp, + }, + headers: { + 'User-Agent': browser, + 'Accept': 'application/json', + 'X-Request-ID': id, + }, + release: 'v1.2.3', + user: { + id: 'user-demo-001', + } as any, + context: { + browser, + os, + screen: browser.includes('Safari') && os.includes('iOS') ? '390x844' : '1920x1080', + timezone: 'UTC+2', + language: 'en-US', + url: 'http://localhost:8080/project/6215743cf3ff6b80215cb183', + }, + addons: {} as any, + }, + catcherType: 'client/javascript', + repetitions: [], + usersAffected: 1, + assignee: undefined as any, + timestamp, + originalTimestamp: timestamp - 86400000, + originalEventId: originalId, +}); + +/** + * Mock data for getRepetitionsPortion - past occurrences of the same error + */ +const mockGetRepetitionsPortion = { + data: { + project: { + event: { + repetitionsPortion: { + repetitions: [ + createRepetitionEvent( + '507f1f77bcf86cd799439014', + '507f1f77bcf86cd799439010', + 'TypeError: Cannot read property \'user\' of undefined', + 'TypeError', + 'hash-507f1f77bcf86cd799439011', + Date.now(), + 42, + 'Chrome 120.0.0 (Windows NT 10.0; Win64; x64)', + 'Windows 10' + ), + createRepetitionEvent( + '507f1f77bcf86cd799439015', + '507f1f77bcf86cd799439010', + 'TypeError: Cannot read property \'user\' of undefined', + 'TypeError', + 'hash-507f1f77bcf86cd799439011', + Date.now() - 3600000, + 39, + 'Safari/537.36 (Macintosh; Intel Mac OS X 10_15_7)', + 'macOS 10.15.7' + ), + createRepetitionEvent( + '507f1f77bcf86cd799439016', + '507f1f77bcf86cd799439010', + 'TypeError: Cannot read property \'user\' of undefined', + 'TypeError', + 'hash-507f1f77bcf86cd799439011', + Date.now() - 7200000, + 37, + 'Mobile Safari 17.2 (iPhone; CPU iPhone OS 17_2 like Mac OS X)', + 'iOS 17.2' + ), + createRepetitionEvent( + '507f1f77bcf86cd799439017', + '507f1f77bcf86cd799439010', + 'TypeError: Cannot read property \'user\' of undefined', + 'TypeError', + 'hash-507f1f77bcf86cd799439011', + Date.now() - 10800000, + 35, + 'Firefox 122.0 (X11; Linux x86_64)', + 'Linux' + ), + ] as HawkEvent[], + nextCursor: null, + }, + }, + }, + }, + errors: [], +}; + +export default mockGetRepetitionsPortion; diff --git a/src/api/events/index.ts b/src/api/events/index.ts index 1296e257a..1a7ad980d 100644 --- a/src/api/events/index.ts +++ b/src/api/events/index.ts @@ -22,6 +22,7 @@ import { import type { User } from '@/types/user'; import type { EventChartItem, ChartLine } from '@/types/chart'; import type { APIResponse } from '../../types/api'; +import { withDemoMock, DEMO_PROJECT_ID } from '@/utils/withDemoMock'; /** * Get specific event @@ -30,7 +31,12 @@ import type { APIResponse } from '../../types/api'; * @param originalEventId - id of the original event * @returns */ -export async function getEvent(projectId: string, eventId: string, originalEventId: string): Promise { +export const getEvent = withDemoMock( + () => import('./getEvent.mock').then(m => m.default), + { + extract: (args) => ({ projectId: args[0] }), + } +)(async function getEvent(projectId: string, eventId: string, originalEventId: string): Promise { const project = await (await api.callOld(QUERY_EVENT, { projectId, eventId, @@ -42,7 +48,7 @@ export async function getEvent(projectId: string, eventId: string, originalEvent } return project.event; -} +}); /** * Returns portion (list) of daily events with pointer to the first daily event of the next portion @@ -53,7 +59,12 @@ export async function getEvent(projectId: string, eventId: string, originalEvent * @param search - search string for daily events * @param release - release identifier to filter events */ -export async function fetchDailyEventsPortion( +export const fetchDailyEventsPortion = withDemoMock( + () => import('./fetchDailyEventsPortion.mock').then(m => m.default), + { + extract: (args) => ({ projectId: args[0] }), + } +)(async function fetchDailyEventsPortion( projectId: string, nextCursor: DailyEventsCursor | null = null, sort = EventsSortOrder.ByDate, @@ -82,9 +93,9 @@ export async function fetchDailyEventsPortion( response.errors.forEach(e => console.error(e)); } - return project?.dailyEventsPortion ?? { cursor: null, - dailyEventsPortion: [] }; -} + return project?.dailyEventsPortion ?? { nextCursor: null, + dailyEvents: [] }; +}); /** * Fetches event's repetitions portion from project @@ -94,7 +105,13 @@ export async function fetchDailyEventsPortion( * @param cursor - the cursor to fetch the next page of repetitions * @returns */ -export async function getRepetitionsPortion( +export const getRepetitionsPortion = withDemoMock( + () => import('./getRepetitionsPortion.mock').then(m => m.default), + { + extract: (args) => ({ projectId: args[0] }), + mapMock: (mock) => mock.default || mock, + } +)(async function getRepetitionsPortion( projectId: string, originalEventId: string, limit: number, cursor?: string ): Promise> { @@ -116,7 +133,7 @@ export async function getRepetitionsPortion( } return response; -} +}); /** * Mark event as visited for current user @@ -124,12 +141,17 @@ export async function getRepetitionsPortion( * @param originalEventId — original event id of the visited one * @returns */ -export async function visitEvent(projectId: string, originalEventId: string): Promise { +export const visitEvent = withDemoMock( + () => import('./visitEvent.mock').then(m => m.default), + { + extract: (args) => ({ projectId: args[0] }), + } +)(async function visitEvent(projectId: string, originalEventId: string): Promise { return (await api.callOld(MUTATION_VISIT_EVENT, { projectId, originalEventId, })).visitEvent; -} +}); /** * Set or unset mark to event @@ -137,13 +159,18 @@ export async function visitEvent(projectId: string, originalEventId: string): Pr * @param eventId — event Id * @param mark — mark to set */ -export async function toggleEventMark(projectId: string, eventId: string, mark: EventMark): Promise { +export const toggleEventMark = withDemoMock( + () => import('./toggleEventMark.mock').then(m => m.default), + { + extract: (args) => ({ projectId: args[0] }), + } +)(async function toggleEventMark(projectId: string, eventId: string, mark: EventMark): Promise { return (await api.callOld(MUTATION_TOGGLE_EVENT_MARK, { projectId, eventId, mark, })).toggleEventMark; -} +}); /** * Update assignee @@ -183,7 +210,12 @@ export async function removeAssignee(projectId: string, eventId: string): Promis * @param days - how many days we need to fetch for displaying in chart * @param timezoneOffset - user's local timezone */ -export async function fetchChartData( +export const fetchChartData = withDemoMock( + () => import('./fetchChartData.mock').then(m => m.default), + { + extract: (args) => ({ projectId: args[0] }), + } +)(async function fetchChartData( projectId: string, originalEventId: string, days: number, @@ -195,4 +227,4 @@ export async function fetchChartData( days, timezoneOffset, })).project.event.chartData; -} +}); diff --git a/src/api/events/toggleEventMark.mock.ts b/src/api/events/toggleEventMark.mock.ts new file mode 100644 index 000000000..5e25c1062 --- /dev/null +++ b/src/api/events/toggleEventMark.mock.ts @@ -0,0 +1,6 @@ +/** + * Mock response for toggleEventMark + */ +const mockToggleEventMark = true; + +export default mockToggleEventMark; diff --git a/src/api/events/visitEvent.mock.ts b/src/api/events/visitEvent.mock.ts new file mode 100644 index 000000000..f68a700ad --- /dev/null +++ b/src/api/events/visitEvent.mock.ts @@ -0,0 +1,6 @@ +/** + * Mock response for visitEvent + */ +const mockVisitEvent = true; + +export default mockVisitEvent; diff --git a/src/api/projects/fetchChartData.mock.js b/src/api/projects/fetchChartData.mock.js new file mode 100644 index 000000000..8596445a8 --- /dev/null +++ b/src/api/projects/fetchChartData.mock.js @@ -0,0 +1,47 @@ +/** + * Mock data for project overview chart + */ +const mockFetchChartData = { + data: { + project: { + chartData: [ + { + label: 'Events', + data: [ + { + timestamp: Date.now() - 6 * 86400000, + count: 15, + }, + { + timestamp: Date.now() - 5 * 86400000, + count: 22, + }, + { + timestamp: Date.now() - 4 * 86400000, + count: 18, + }, + { + timestamp: Date.now() - 3 * 86400000, + count: 25, + }, + { + timestamp: Date.now() - 2 * 86400000, + count: 31, + }, + { + timestamp: Date.now() - 1 * 86400000, + count: 28, + }, + { + timestamp: Date.now(), + count: 35, + }, + ], + }, + ], + }, + }, + errors: [], +}; + +export default mockFetchChartData; diff --git a/src/api/projects/fetchProjectReleaseDetails.mock.ts b/src/api/projects/fetchProjectReleaseDetails.mock.ts new file mode 100644 index 000000000..99df956f3 --- /dev/null +++ b/src/api/projects/fetchProjectReleaseDetails.mock.ts @@ -0,0 +1,74 @@ +/** + * Mock data for fetchProjectReleaseDetails + */ +const mockFetchProjectReleaseDetails = { + data: { + project: { + releaseDetails: { + id: 'release-rel-001', + release: 'v1.2.3', + version: '1.2.3', + created: new Date('2026-01-28'), + description: 'Latest stable release with bug fixes and performance improvements', + commits: 24, + filesChanged: 156, + insertions: 2841, + deletions: 1205, + author: 'John Developer', + authorEmail: 'dev@example.com', + changelog: ` +## v1.2.3 - 2026-01-28 + +### New Features +- Added event filtering by browser type +- Implemented real-time notifications for critical errors +- New dashboard widget for error trends + +### Bug Fixes +- Fixed crash when loading events without timezone data +- Resolved memory leak in event storage +- Fixed typo in notification templates + +### Performance +- Optimized database queries for large datasets +- Reduced bundle size by 15% +- Improved chart rendering performance + +### Breaking Changes +- Removed deprecated API endpoints from v1.1.x +- Updated minimum Node.js version to 18.0.0 + +### Contributors +- John Developer (@johndev) +- Sarah Team Lead (@sarahteam) +- Alex Frontend (@alexfrontend) + `, + eventGroupingPatterns: [ + { + id: 'pattern-001', + regexp: 'TypeError.*Cannot read.*undefined', + template: '[TypeError] Cannot read property', + }, + { + id: 'pattern-002', + regexp: 'ReferenceError.*is not defined', + template: '[ReferenceError] Variable not defined', + }, + { + id: 'pattern-003', + regexp: 'SyntaxError.*Unexpected token', + template: '[SyntaxError] Parser error', + }, + { + id: 'pattern-004', + regexp: 'NetworkError.*Failed to fetch', + template: '[NetworkError] Request failed', + }, + ], + }, + }, + }, + errors: [], +}; + +export default mockFetchProjectReleaseDetails; diff --git a/src/api/projects/fetchProjectReleases.mock.ts b/src/api/projects/fetchProjectReleases.mock.ts new file mode 100644 index 000000000..945967bb7 --- /dev/null +++ b/src/api/projects/fetchProjectReleases.mock.ts @@ -0,0 +1,66 @@ +/** + * Mock data for fetchProjectReleases + */ +const mockFetchProjectReleases = { + data: { + project: { + releases: [ + { + id: 'release-rel-001', + release: 'v1.2.3', + version: '1.2.3', + created: new Date('2026-01-28'), + description: 'Latest stable release with bug fixes and performance improvements', + eventGroupingPatterns: [], + commits: 24, + filesChanged: 156, + insertions: 2841, + deletions: 1205, + author: 'John Developer', + }, + { + id: 'release-rel-002', + release: 'v1.2.2', + version: '1.2.2', + created: new Date('2026-01-21'), + description: 'Hotfix release - critical security patches', + eventGroupingPatterns: [], + commits: 5, + filesChanged: 8, + insertions: 142, + deletions: 89, + author: 'Sarah Team Lead', + }, + { + id: 'release-rel-003', + release: 'v1.2.1', + version: '1.2.1', + created: new Date('2026-01-14'), + description: 'Minor release - new features and improvements', + eventGroupingPatterns: [], + commits: 38, + filesChanged: 203, + insertions: 4156, + deletions: 1876, + author: 'John Developer', + }, + { + id: 'release-rel-004', + release: 'v1.2.0', + version: '1.2.0', + created: new Date('2026-01-07'), + description: 'Major release - new API endpoints and database schema', + eventGroupingPatterns: [], + commits: 87, + filesChanged: 451, + insertions: 12456, + deletions: 5643, + author: 'Sarah Team Lead', + }, + ], + }, + }, + errors: [], +}; + +export default mockFetchProjectReleases; diff --git a/src/api/projects/getProjects.mock.ts b/src/api/projects/getProjects.mock.ts new file mode 100644 index 000000000..c9c68d227 --- /dev/null +++ b/src/api/projects/getProjects.mock.ts @@ -0,0 +1,8 @@ +import { DEMO_PROJECT } from './getWorkspaces.mock'; + +/** + * Mock for projects list in workspace + */ +const mockProjectsList = [DEMO_PROJECT]; + +export default mockProjectsList; diff --git a/src/api/projects/index.js b/src/api/projects/index.js index 4ce26120f..fb2067b95 100644 --- a/src/api/projects/index.js +++ b/src/api/projects/index.js @@ -20,6 +20,7 @@ import { MUTATION_UPDATE_TASK_MANAGER_SETTINGS } from './queries'; import * as api from '../index.ts'; +import { withDemoMock, DEMO_PROJECT_ID } from '@/utils/withDemoMock'; /** * Create project and returns its id @@ -128,9 +129,20 @@ export async function removeProject(projectId) { * @param {string} projectId - project ID * @returns {Promise} */ -export async function updateLastProjectVisit(projectId) { +/** + * Update last project visit timestamp + * + * @param {string} projectId - id of the project + * @returns {Promise} - success status + */ +export const updateLastProjectVisit = withDemoMock( + () => Promise.resolve(true), + { + extract: (args) => ({ projectId: args[0] }), + } +)(async function updateLastProjectVisit(projectId) { return (await api.callOld(MUTATION_UPDATE_LAST_VISIT, { projectId })).setLastProjectVisit; -} +}); /** * Send request for creation new project notifications rule @@ -238,7 +250,22 @@ export async function toggleEnabledStateOfProjectNotificationsRule(payload) { * @param {number} timezoneOffset - user's local timezone offset * @returns {Promise>} */ -export async function fetchChartData(projectId, startDate, endDate, groupBy, timezoneOffset) { +/** + * Fetch chart data for project overview + * + * @param {string} projectId - id of the project + * @param {string} startDate - start date in ISO format + * @param {string} endDate - end date in ISO format + * @param {string} groupBy - grouping period (day, week, month) + * @param {number} timezoneOffset - timezone offset in minutes + * @returns {Promise} - chart data response + */ +export const fetchChartData = withDemoMock( + () => import('./fetchChartData.mock').then(m => m.default), + { + extract: (args) => ({ projectId: args[0] }), + } +)(async function fetchChartData(projectId, startDate, endDate, groupBy, timezoneOffset) { const response = await api.call(QUERY_CHART_DATA, { projectId, startDate, @@ -253,7 +280,7 @@ export async function fetchChartData(projectId, startDate, endDate, groupBy, tim }); return response; -} +}); /** * Fetch project releases @@ -261,7 +288,13 @@ export async function fetchChartData(projectId, startDate, endDate, groupBy, tim * @param {string} projectId - id of the project to fetch releases * @returns {Promise>} - list of releases with unique events count, commits count and files count */ -export async function fetchProjectReleases(projectId) { +export const fetchProjectReleases = withDemoMock( + () => import('./fetchProjectReleases.mock'), + { + extract: (args) => ({ projectId: args[0] }), + mapMock: (mock) => (mock.default || mock).data.project.releases, + } +)(async function fetchProjectReleases(projectId) { const response = await api.call(QUERY_PROJECT_RELEASES, { projectId }); if (response.errors?.length) { @@ -269,7 +302,7 @@ export async function fetchProjectReleases(projectId) { } return response.data.project.releases; -} +}); /** * Fetch specific release details @@ -278,7 +311,13 @@ export async function fetchProjectReleases(projectId) { * @param {string} release * @returns {Promise} */ -export async function fetchProjectReleaseDetails(projectId, release) { +export const fetchProjectReleaseDetails = withDemoMock( + () => import('./fetchProjectReleaseDetails.mock'), + { + extract: (args) => ({ projectId: args[0] }), + mapMock: (mock) => (mock.default || mock).data.project.releaseDetails, + } +)(async function fetchProjectReleaseDetails(projectId, release) { const response = await api.call(QUERY_PROJECT_RELEASE_DETAILS, { projectId, release }); @@ -294,7 +333,7 @@ export async function fetchProjectReleaseDetails(projectId, release) { } return response.data.project.releaseDetails; -} +}); /** * Send request for unsubscribing from notifications diff --git a/src/api/user/demoUser.mock.ts b/src/api/user/demoUser.mock.ts new file mode 100644 index 000000000..4845ce066 --- /dev/null +++ b/src/api/user/demoUser.mock.ts @@ -0,0 +1,13 @@ +import type { User } from '@/types/user'; + +/** + * Demo user for authentication mocks + */ +export const DEMO_USER: User = { + id: '6215743cf3ff6b80215cb185', + email: 'demo@hawk.com', + name: 'Demo User', + image: 'https://ui-avatars.com/api/?name=Demo+User&background=random', +}; + +export default DEMO_USER; diff --git a/src/api/user/fetchBankCards.mock.ts b/src/api/user/fetchBankCards.mock.ts new file mode 100644 index 000000000..4b23a1708 --- /dev/null +++ b/src/api/user/fetchBankCards.mock.ts @@ -0,0 +1,25 @@ +/** + * Mock bank cards for demo user + */ +const mockBankCards = { + me: { + bankCards: [ + { + id: 'card-demo-1', + cardNumber: '411111****1111', + expiryDate: '12/25', + cardHolder: 'Demo User', + isDefault: true, + }, + { + id: 'card-demo-2', + cardNumber: '511111****1111', + expiryDate: '06/26', + cardHolder: 'Demo User', + isDefault: false, + }, + ], + }, +}; + +export default mockBankCards; diff --git a/src/api/user/fetchCurrentUser.mock.ts b/src/api/user/fetchCurrentUser.mock.ts new file mode 100644 index 000000000..f907d3ea1 --- /dev/null +++ b/src/api/user/fetchCurrentUser.mock.ts @@ -0,0 +1,27 @@ +/** + * Mock response for fetchCurrentUser + */ +const mockCurrentUser = { + data: { + me: { + id: '6215743cf3ff6b80215cb185', + email: 'demo@hawk.com', + name: 'Demo User', + image: 'https://ui-avatars.com/api/?name=Demo+User&background=random', + notifications: { + channels: { + email: true, + slack: false, + telegram: false, + }, + receiveType: { + onlyNew: false, + onlyAssigned: false, + }, + }, + }, + }, + errors: [], +}; + +export default mockCurrentUser; diff --git a/src/api/user/index.js b/src/api/user/index.js index 6b4f7eecd..1ff5a6fd4 100644 --- a/src/api/user/index.js +++ b/src/api/user/index.js @@ -12,6 +12,7 @@ import { } from './queries'; import * as api from '../index.ts'; import { validateUtmParams } from '../../components/utils/utm/utm.ts'; +import { withDemoMock, DEMO_PROJECT_ID } from '../../utils/withDemoMock'; /** * @typedef {object} TokensPair @@ -26,12 +27,18 @@ import { validateUtmParams } from '../../components/utils/utm/utm.ts'; * @param {string} password - Password * @returns {Promise<{data: {login: TokensPair}, errors: object[]}>} - Auth token */ -export async function login(email, password) { +export const login = withDemoMock( + () => import('./login.mock'), + { + extract: () => ({ projectId: DEMO_PROJECT_ID }), + mapMock: (mock) => mock.default || mock, + } +)(async function login(email, password) { return api.call(MUTATION_LOGIN, { email, password, }); -} +}); /** * Sign up by email and return status (true or false) @@ -40,7 +47,13 @@ export async function login(email, password) { * @param {object} utm - UTM parameters object * @returns {Promise<{data: {signUp: boolean}, errors: object[]}>} Response data */ -export async function signUp(email, utm) { +export const signUp = withDemoMock( + () => import('./signUp.mock'), + { + extract: () => ({ projectId: DEMO_PROJECT_ID }), + mapMock: (mock) => mock.default || mock, + } +)(async function signUp(email, utm) { const validatedUtm = validateUtmParams(utm); const variables = { @@ -49,7 +62,7 @@ export async function signUp(email, utm) { }; return api.call(MUTATION_SIGN_UP, variables); -} +}); /** * Recover password by email @@ -83,9 +96,15 @@ export async function refreshTokens(refreshToken) { * * @returns {Promise>} */ -export async function fetchCurrentUser() { +export const fetchCurrentUser = withDemoMock( + () => import('./fetchCurrentUser.mock'), + { + extract: () => ({ projectId: DEMO_PROJECT_ID }), + mapMock: (mock) => mock.default || mock, + } +)(async function fetchCurrentUser() { return await api.call(QUERY_CURRENT_USER, {}, undefined, { allowErrors: true }); -} +}); /** * Update user profile @@ -154,6 +173,12 @@ export async function updateNotificationsReceiveType(payload) { * * @returns {Promise>} */ -export async function fetchBankCards() { +export const fetchBankCards = withDemoMock( + () => import('./fetchBankCards.mock'), + { + extract: () => ({ projectId: DEMO_PROJECT_ID }), + mapMock: (mock) => (mock.default || mock).me.bankCards, + } +)(async function fetchBankCards() { return (await api.callOld(QUERY_BANK_CARDS)).me.bankCards || []; -} +}); diff --git a/src/api/user/login.mock.ts b/src/api/user/login.mock.ts new file mode 100644 index 000000000..086876d6c --- /dev/null +++ b/src/api/user/login.mock.ts @@ -0,0 +1,14 @@ +/** + * Mock tokens for demo authentication + */ +const mockTokens = { + data: { + login: { + accessToken: 'demo-access-token-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', + refreshToken: 'demo-refresh-token-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', + }, + }, + errors: [], +}; + +export default mockTokens; diff --git a/src/api/user/signUp.mock.ts b/src/api/user/signUp.mock.ts new file mode 100644 index 000000000..3a26ecb36 --- /dev/null +++ b/src/api/user/signUp.mock.ts @@ -0,0 +1,11 @@ +/** + * Mock response for sign up + */ +const mockSignUp = { + data: { + signUp: true, + }, + errors: [], +}; + +export default mockSignUp; diff --git a/src/api/workspaces/getAllWorkspacesWithProjects.mock.ts b/src/api/workspaces/getAllWorkspacesWithProjects.mock.ts new file mode 100644 index 000000000..e4f3e9f11 --- /dev/null +++ b/src/api/workspaces/getAllWorkspacesWithProjects.mock.ts @@ -0,0 +1,77 @@ +import type { Workspace } from '@/types/workspaces'; +import { DEMO_PROJECT_ID, DEMO_WORKSPACE_ID } from '@/utils/withDemoMock'; +import { DEMO_USER } from '@/api/user/demoUser.mock'; +import { DEMO_PROJECT } from './getWorkspaces.mock'; +import mockDailyEventsPortion from '@/api/events/fetchDailyEventsPortion.mock'; + +/** + * Demo workspace with demo project + */ +export const DEMO_WORKSPACE: Workspace = { + id: DEMO_WORKSPACE_ID, + workspaceId: DEMO_WORKSPACE_ID, + name: 'Demo Workspace - Error Tracking Hub', + description: 'Comprehensive workspace for monitoring and managing application errors in real-time', + image: 'https://ui-avatars.com/api/?name=Demo+Workspace&background=FF6B6B', + inviteHash: 'demo-invite-hash-abc123def456', + team: [ + { + id: 'member-001-admin', + user: DEMO_USER, + isAdmin: true, + }, + { + id: 'member-002-dev', + user: { + id: 'dev-user-001', + email: 'dev@example.com', + name: 'John Developer', + image: 'https://ui-avatars.com/api/?name=John+Developer&background=4ECDC4', + }, + isAdmin: false, + }, + { + id: 'member-003-lead', + user: { + id: 'lead-user-001', + email: 'team-lead@example.com', + name: 'Sarah Team Lead', + image: 'https://ui-avatars.com/api/?name=Sarah+Lead&background=95E1D3', + }, + isAdmin: true, + }, + ], + plan: { + id: 'pro-plan-001', + name: 'Professional', + monthlyCount: 100000, + priceMonthly: 99, + priceYearly: 990, + }, + lastChargeDate: new Date('2026-01-15'), + isDebug: true, + isBlocked: false, +}; + +/** + * Enrich project with daily events portion for display in workspace + * Create fresh copy to prevent mutation + */ +const projectWithEvents = { + ...DEMO_PROJECT, + dailyEventsPortion: mockDailyEventsPortion(), +}; + +const mockAllWorkspacesWithProjects = { + data: { + workspaces: [ + { + ...DEMO_WORKSPACE, + projects: [projectWithEvents], + }, + ], + }, + errors: [], +}; + +export default mockAllWorkspacesWithProjects; diff --git a/src/api/workspaces/getWorkspaces.mock.ts b/src/api/workspaces/getWorkspaces.mock.ts new file mode 100644 index 000000000..fd9f462be --- /dev/null +++ b/src/api/workspaces/getWorkspaces.mock.ts @@ -0,0 +1,80 @@ +import type { Project } from '@/types/project'; +import { DEMO_PROJECT_ID, DEMO_WORKSPACE_ID } from '@/utils/withDemoMock'; +import { DEMO_USER } from '@/api/user/demoUser.mock'; +import mockDailyEventsPortion from '@/api/events/fetchDailyEventsPortion.mock'; + +/** + * Demo project for workspace with comprehensive mock data + */ +export const DEMO_PROJECT: Project = { + id: DEMO_PROJECT_ID, + workspaceId: DEMO_WORKSPACE_ID, + token: 'token_6215743cf3ff6b80215cb183_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJpc3MiOiJodHRwczovL2hhd2suc28iLCJzdWIiOiIyMjQ2MjExODUyMjcwMzY4MzIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE2MDMzOTAwMDB9', + name: 'Production Application', + uidAdded: DEMO_USER, + unreadCount: 5, + description: 'Production environment - real-time error tracking and monitoring for main application', + image: 'https://ui-avatars.com/api/?name=Production+App&background=FF6B6B', + notifications: [ + { + id: 'notif-001', + rule: { + channels: { + email: true, + slack: true, + telegram: false, + }, + receiveType: { + onlyNew: false, + onlyAssigned: false, + }, + }, + }, + ], + eventGroupingPatterns: [ + { + id: 'pattern-001', + regexp: 'TypeError.*Cannot read.*undefined', + template: '[TypeError] Cannot read property', + }, + { + id: 'pattern-002', + regexp: 'ReferenceError.*is not defined', + template: '[ReferenceError] Variable not defined', + }, + { + id: 'pattern-003', + regexp: 'SyntaxError.*Unexpected token', + template: '[SyntaxError] Parser error', + }, + ], + rateLimitSettings: { + N: 100000, + T: 86400, + }, +}; + +/** + * Enrich project with daily events portion for display in workspace + */ +const projectWithEvents = { + ...DEMO_PROJECT, + dailyEventsPortion: mockDailyEventsPortion, +}; + +/** + * Demo workspace with demo project + */ +const mockWorkspaceWithProject = { + data: { + workspaces: [ + { + id: DEMO_WORKSPACE_ID, + projects: [projectWithEvents], + }, + ], + }, + errors: [], +}; + +export default mockWorkspaceWithProject; diff --git a/src/api/workspaces/index.ts b/src/api/workspaces/index.ts index 1a4b63422..705c8e4e2 100644 --- a/src/api/workspaces/index.ts +++ b/src/api/workspaces/index.ts @@ -24,6 +24,7 @@ import type { WorkspaceSsoConfigInput } from '@/types/workspaces'; import type { APIResponse, APIResponseData } from '@/types/api'; +import { withDemoMock, DEMO_WORKSPACE_ID } from '@/utils/withDemoMock'; interface CreateWorkspaceInput { /** @@ -60,7 +61,13 @@ export async function leaveWorkspace(workspaceId: string): Promise { * Returns all user's workspaces and project. * @returns */ -export async function getAllWorkspacesWithProjects(): Promise> { +export const getAllWorkspacesWithProjects = withDemoMock( + () => import('./getAllWorkspacesWithProjects.mock'), + { + extract: () => ({ workspaceId: DEMO_WORKSPACE_ID }), + mapMock: (mock) => mock.default || mock, + } +)(async function getAllWorkspacesWithProjects(): Promise> { return api.call(QUERY_ALL_WORKSPACES_WITH_PROJECTS, undefined, undefined, { initial: true, @@ -70,7 +77,7 @@ export async function getAllWorkspacesWithProjects(): Promise { +export const getWorkspaces = withDemoMock( + () => import('./getWorkspaces.mock'), + { + extract: (args) => ({ workspaceId: args[0]?.[0] }), + mapMock: (mock) => ({ workspaces: (mock.default || mock).data.workspaces }), + } +)(async function getWorkspaces(ids: string[]): Promise { return (await api.callOld(QUERY_WORKSPACES, { ids })).workspaces; -} +}); /** * Get workspace balance diff --git a/src/components/project/EventsList.vue b/src/components/project/EventsList.vue index 5a53baf0e..a1d650351 100644 --- a/src/components/project/EventsList.vue +++ b/src/components/project/EventsList.vue @@ -19,7 +19,7 @@ { + console.log(` ✅ [${i}] eventId=${de.eventId}`); + }); + this.dailyEventsNextCursor = nextCursor; this.noMore = this.dailyEventsNextCursor === null; @@ -299,6 +313,8 @@ export default { this.dailyEvents.push(...dailyEventsWithEventsLinked); } + console.log('[✅ loadMoreEvents] After push: dailyEvents.length=', this.dailyEvents.length); + this.isLoading = false; }, /** @@ -349,6 +365,12 @@ export default { */ onShowEventOverview(eventId) { const event = this.getEvent(eventId); + + if (!event) { + console.error('[EventsList] onShowEventOverview: event not found for eventId:', eventId); + return; + } + const originalEventId = event.originalEventId; if (this.isAssigneesShowed) { diff --git a/src/main.ts b/src/main.ts index baf651043..1929e6c71 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,6 +10,7 @@ import { i18n } from './i18n'; import * as api from './api/index'; import { REFRESH_TOKENS } from './store/modules/user/actionTypes'; import { RESET_STORE } from './store/methodsTypes'; +import { enableDemoMode } from './utils/withDemoMock'; import '@codexteam/ui/styles'; @@ -28,6 +29,17 @@ import setupDirectives from './directives'; const { init: initHawk, track } = useErrorTracker(); +/** + * Enable demo mode if running without a server + * You can disable this in production or enable via console: enableDemoMode() + */ +console.log('[App Init] VITE_API_URL:', import.meta.env.VITE_API_URL); +if (!import.meta.env.VITE_API_URL || import.meta.env.VITE_API_URL === 'http://localhost:4000') { + enableDemoMode(); +} else { + console.log('[App Init] Using real API server at:', import.meta.env.VITE_API_URL); +} + const app = createApp(App); app.use(router); @@ -119,3 +131,13 @@ api.setupApiModuleHandlers({ }); }, DEBOUNCE_TIMEOUT), }); + +/** + * Export demo mode functions to window for browser console usage + * Usage: enableDemoMode() or disableDemoMode() in browser console + */ +(window as any).enableDemoMode = enableDemoMode; +(window as any).disableDemoMode = () => { + const { disableDemoMode } = require('./utils/withDemoMock'); + disableDemoMode(); +}; diff --git a/src/store/modules/events/index.ts b/src/store/modules/events/index.ts index 98200153c..18c785ba3 100644 --- a/src/store/modules/events/index.ts +++ b/src/store/modules/events/index.ts @@ -179,8 +179,16 @@ const module: Module = { */ return (projectId: string, eventId: string): HawkEvent | null => { const key = getEventsListKey(projectId, eventId); + const event = state.events[key] || null; - return state.events[key] || null; + if (!event) { + const availableKeys = Object.keys(state.events).filter(k => k.startsWith(projectId + ':')); + console.warn(`[⚠️ getProjectEventById] NOT FOUND: key="${key}". Available keys for ${projectId}:`, availableKeys); + } else { + console.log(`[✅ getProjectEventById] FOUND: ${key}`); + } + + return event; }; }, @@ -271,6 +279,20 @@ const module: Module = { const dailyEvents = dailyEventsPortion.dailyEvents; + // DEBUG: Check if dailyEvents exist and have event field + console.log('[🔍 FETCH_PROJECT_OVERVIEW] Received from API:', { + projectId, + dailyEventsCount: dailyEvents.length, + }); + + dailyEvents.forEach((de, i) => { + if (de.event?.id) { + console.log(` ✅ [${i}] event.id = ${de.event.id}`); + } else { + console.error(` ❌ [${i}] MISSING event.id in dailyEvent`, de); + } + }); + /** * Reset loadedEventsCount only when starting a new search * This ensures proper pagination during search @@ -590,14 +612,24 @@ const module: Module = { projectId: string; eventsList: HawkEvent[]; }): void { - const additions = Object.fromEntries(eventsList.map((event) => { - return [getEventsListKey(projectId, event.id), event]; + console.log(`[🔍 AddToEventsList] Adding ${eventsList.length} events for project ${projectId}`); + + const additions = Object.fromEntries(eventsList.map((event, idx) => { + const key = getEventsListKey(projectId, event.id); + if (!event.id) { + console.error(` ❌ [${idx}] Event missing id!`, event); + } else { + console.log(` ✅ [${idx}] ${key} => event.id=${event.id}`); + } + return [key, event]; })); state.events = { ...state.events, ...additions, }; + + console.log(`[✅ AddToEventsList] state.events now has ${Object.keys(state.events).length} keys`); }, /** diff --git a/src/utils/withDemoMock.ts b/src/utils/withDemoMock.ts new file mode 100644 index 000000000..902e24ec7 --- /dev/null +++ b/src/utils/withDemoMock.ts @@ -0,0 +1,182 @@ +/** + * Demo workspace and project IDs used for mocking + */ +export const DEMO_PROJECT_ID = '6215743cf3ff6b80215cb183'; +export const DEMO_WORKSPACE_ID = '6213b6a01e6281087467cc7a'; + +/** + * Global flag to force demo mode (for development/testing without server) + * Set to true to always use mock data instead of real API + */ +export let FORCE_DEMO_MODE = false; + +/** + * Enable demo mode globally + */ +export function enableDemoMode() { + FORCE_DEMO_MODE = true; + console.log('[Demo Mode] Enabled - all API calls will use mock data'); +} + +/** + * Disable demo mode globally + */ +export function disableDemoMode() { + FORCE_DEMO_MODE = false; + console.log('[Demo Mode] Disabled - API calls will use real server'); +} + +type MaybePromise = T | Promise; + +/** + * Options for the demo mock decorator + */ +export type DemoMockOptions any> = { + /** + * How to extract projectId/workspaceId from function arguments. + * Default: projectId = args[0] + */ + extract?: (args: Parameters) => { projectId?: string; workspaceId?: string }; + + /** + * Enable/disable mocks (e.g., disable in production) + * Default: true + */ + enabled?: boolean | (() => boolean); + + /** + * Transform the mock data before returning + * Default: return mock as-is + */ + mapMock?: (mock: any, ctx: DemoContext) => any; +}; + +/** + * Context passed to mock factory and mapMock + */ +export type DemoContext any> = { + args: Parameters; + projectId?: string; + workspaceId?: string; + original: Fn; +}; + +/** + * Decorator that intercepts API calls for demo workspace/project and returns mock data instead + * + * @example + * ```ts + * @withDemoMock( + * () => import('./fetchData.mock').then(m => m.default), + * { extract: (args) => ({ projectId: args[0] }) } + * ) + * export async function fetchData(projectId: string) { + * // real API call + * } + * ``` + * + * @param mockOrFactory - Mock data or lazy import factory function + * @param options - Configuration options + */ +export function withDemoMock any>( + mockOrFactory: any | ((ctx: DemoContext) => MaybePromise), + options: DemoMockOptions = {} +) { + const { + extract = ((args: any[]) => ({ projectId: args?.[0] })) as any, + enabled = true, + mapMock = (m) => m, + } = options; + + return function decorator(original: Fn, _context?: any) { + // If not a function, return as-is + if (typeof original !== 'function') return original; + + return async function (this: any, ...args: Parameters): Promise>> { + // Check if mocks are enabled + const isEnabled = typeof enabled === 'function' ? enabled() : enabled; + if (!isEnabled) { + return original.apply(this, args); + } + + // Extract IDs from arguments + const ids = extract(args) ?? {}; + const isDemo = + FORCE_DEMO_MODE || + ids.projectId === DEMO_PROJECT_ID || + ids.workspaceId === DEMO_WORKSPACE_ID; + + // If not demo, call original function + if (!isDemo) { + return original.apply(this, args); + } + + console.log(`[Demo Mock] ✓ Using mock for ${original.name}`, { projectId: ids.projectId, workspaceId: ids.workspaceId }); + + // Build context + const ctx: DemoContext = { + args, + projectId: ids.projectId, + workspaceId: ids.workspaceId, + original, + }; + + // Resolve mock data (supports lazy imports) + let rawMock; + if (typeof mockOrFactory === 'function') { + const result = await (mockOrFactory as any)(ctx); + // Support for ES modules with default export + rawMock = result?.default ?? result; + } else { + rawMock = mockOrFactory; + } + + // If rawMock is a function, call it to get the actual data (prevents mutation) + if (typeof rawMock === 'function') { + rawMock = rawMock(); + } + + // If no mock provided, fall back to real API + if (rawMock == null) { + console.warn(`[Demo Mock] ⚠ No mock data found for ${original.name}, falling back to real API`); + return original.apply(this, args); + } + + // Apply transformation and return + const result = mapMock(rawMock, ctx) as Awaited>; + + // Validate structure for fetchDailyEventsPortion BEFORE returning + if (original.name === 'fetchDailyEventsPortion') { + console.log('[Demo Mock] ⚠️ CRITICAL: Validating fetchDailyEventsPortion structure BEFORE store mutation'); + const dailyEvents = (result as any).dailyEvents; + if (!dailyEvents) { + console.error('[Demo Mock] ERROR: No dailyEvents found in result!', result); + } else { + console.log(`[Demo Mock] dailyEvents count: ${dailyEvents.length}`); + dailyEvents.forEach((de: any, idx: number) => { + console.log(` [${idx}]:`, { + id: de.id, + groupingTimestamp: de.groupingTimestamp, + count: de.count, + affectedUsers: de.affectedUsers, + hasEvent: !!de.event, + eventId: de.event?.id, + eventKeys: de.event ? Object.keys(de.event).slice(0, 3) : 'N/A', + }); + if (!de.event) { + console.error(`[Demo Mock] ERROR: dailyEvents[${idx}].event is missing!`); + } + if (!de.event?.id) { + console.error(`[Demo Mock] ERROR: dailyEvents[${idx}].event.id is missing!`); + } + }); + } + } + + // Log without deep cloning (avoid JSON.parse issues with functions/undefined) + console.log(`[Demo Mock] → Returning mock data for ${original.name}:`, result); + + return result; + } as Fn; + }; +} From 11ad11fc577e0c7ad9f217f1ef5e45f4b871dec5 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 7 Feb 2026 13:11:42 +0300 Subject: [PATCH 02/44] remove demo user --- .../events/fetchDailyEventsPortion.mock.ts | 3 +- src/api/events/getEvent.mock.ts | 3 +- src/api/events/getRepetitionsPortion.mock.ts | 3 +- src/api/user/demoUser.mock.ts | 13 ---- src/api/user/fetchBankCards.mock.ts | 25 ------- src/api/user/fetchCurrentUser.mock.ts | 27 ------- src/api/user/index.js | 41 +++-------- src/api/user/login.mock.ts | 14 ---- src/api/user/signUp.mock.ts | 11 --- .../getAllWorkspacesWithProjects.mock.ts | 72 ++++++++++--------- src/api/workspaces/getWorkspaces.mock.ts | 31 +++----- src/api/workspaces/index.ts | 35 ++++----- 12 files changed, 75 insertions(+), 203 deletions(-) delete mode 100644 src/api/user/demoUser.mock.ts delete mode 100644 src/api/user/fetchBankCards.mock.ts delete mode 100644 src/api/user/fetchCurrentUser.mock.ts delete mode 100644 src/api/user/login.mock.ts delete mode 100644 src/api/user/signUp.mock.ts diff --git a/src/api/events/fetchDailyEventsPortion.mock.ts b/src/api/events/fetchDailyEventsPortion.mock.ts index a0245d830..fca753d76 100644 --- a/src/api/events/fetchDailyEventsPortion.mock.ts +++ b/src/api/events/fetchDailyEventsPortion.mock.ts @@ -1,5 +1,4 @@ import type { DailyEventsPortion, HawkEvent } from '@/types/events'; -import { DEMO_USER } from '@/api/user/demoUser.mock'; /** * Create a complete mock event with all fields @@ -16,7 +15,7 @@ const createDemoEvent = ( id, groupHash, totalCount, - visitedBy: [DEMO_USER], + visitedBy: [], marks: { resolved: false, starred: false, diff --git a/src/api/events/getEvent.mock.ts b/src/api/events/getEvent.mock.ts index fe5588d6c..e56e9b30a 100644 --- a/src/api/events/getEvent.mock.ts +++ b/src/api/events/getEvent.mock.ts @@ -1,5 +1,4 @@ import type { HawkEvent } from '@/types/events'; -import { DEMO_USER } from '@/api/user/demoUser.mock'; /** * Mock data for getEvent - detailed event information @@ -77,7 +76,7 @@ const mockGetEvent: HawkEvent = { }, catcherType: 'client/javascript', repetitions: [], - visitedBy: [DEMO_USER], + visitedBy: [], marks: { resolved: false, starred: true, diff --git a/src/api/events/getRepetitionsPortion.mock.ts b/src/api/events/getRepetitionsPortion.mock.ts index 95e980427..469d523a4 100644 --- a/src/api/events/getRepetitionsPortion.mock.ts +++ b/src/api/events/getRepetitionsPortion.mock.ts @@ -1,5 +1,4 @@ import type { HawkEvent } from '@/types/events'; -import { DEMO_USER } from '@/api/user/demoUser.mock'; /** * Helper to create detailed mock repetition events @@ -18,7 +17,7 @@ const createRepetitionEvent = ( id, groupHash, totalCount: count, - visitedBy: [DEMO_USER], + visitedBy: [], marks: { resolved: false, starred: false, diff --git a/src/api/user/demoUser.mock.ts b/src/api/user/demoUser.mock.ts deleted file mode 100644 index 4845ce066..000000000 --- a/src/api/user/demoUser.mock.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { User } from '@/types/user'; - -/** - * Demo user for authentication mocks - */ -export const DEMO_USER: User = { - id: '6215743cf3ff6b80215cb185', - email: 'demo@hawk.com', - name: 'Demo User', - image: 'https://ui-avatars.com/api/?name=Demo+User&background=random', -}; - -export default DEMO_USER; diff --git a/src/api/user/fetchBankCards.mock.ts b/src/api/user/fetchBankCards.mock.ts deleted file mode 100644 index 4b23a1708..000000000 --- a/src/api/user/fetchBankCards.mock.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Mock bank cards for demo user - */ -const mockBankCards = { - me: { - bankCards: [ - { - id: 'card-demo-1', - cardNumber: '411111****1111', - expiryDate: '12/25', - cardHolder: 'Demo User', - isDefault: true, - }, - { - id: 'card-demo-2', - cardNumber: '511111****1111', - expiryDate: '06/26', - cardHolder: 'Demo User', - isDefault: false, - }, - ], - }, -}; - -export default mockBankCards; diff --git a/src/api/user/fetchCurrentUser.mock.ts b/src/api/user/fetchCurrentUser.mock.ts deleted file mode 100644 index f907d3ea1..000000000 --- a/src/api/user/fetchCurrentUser.mock.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Mock response for fetchCurrentUser - */ -const mockCurrentUser = { - data: { - me: { - id: '6215743cf3ff6b80215cb185', - email: 'demo@hawk.com', - name: 'Demo User', - image: 'https://ui-avatars.com/api/?name=Demo+User&background=random', - notifications: { - channels: { - email: true, - slack: false, - telegram: false, - }, - receiveType: { - onlyNew: false, - onlyAssigned: false, - }, - }, - }, - }, - errors: [], -}; - -export default mockCurrentUser; diff --git a/src/api/user/index.js b/src/api/user/index.js index 1ff5a6fd4..6b4f7eecd 100644 --- a/src/api/user/index.js +++ b/src/api/user/index.js @@ -12,7 +12,6 @@ import { } from './queries'; import * as api from '../index.ts'; import { validateUtmParams } from '../../components/utils/utm/utm.ts'; -import { withDemoMock, DEMO_PROJECT_ID } from '../../utils/withDemoMock'; /** * @typedef {object} TokensPair @@ -27,18 +26,12 @@ import { withDemoMock, DEMO_PROJECT_ID } from '../../utils/withDemoMock'; * @param {string} password - Password * @returns {Promise<{data: {login: TokensPair}, errors: object[]}>} - Auth token */ -export const login = withDemoMock( - () => import('./login.mock'), - { - extract: () => ({ projectId: DEMO_PROJECT_ID }), - mapMock: (mock) => mock.default || mock, - } -)(async function login(email, password) { +export async function login(email, password) { return api.call(MUTATION_LOGIN, { email, password, }); -}); +} /** * Sign up by email and return status (true or false) @@ -47,13 +40,7 @@ export const login = withDemoMock( * @param {object} utm - UTM parameters object * @returns {Promise<{data: {signUp: boolean}, errors: object[]}>} Response data */ -export const signUp = withDemoMock( - () => import('./signUp.mock'), - { - extract: () => ({ projectId: DEMO_PROJECT_ID }), - mapMock: (mock) => mock.default || mock, - } -)(async function signUp(email, utm) { +export async function signUp(email, utm) { const validatedUtm = validateUtmParams(utm); const variables = { @@ -62,7 +49,7 @@ export const signUp = withDemoMock( }; return api.call(MUTATION_SIGN_UP, variables); -}); +} /** * Recover password by email @@ -96,15 +83,9 @@ export async function refreshTokens(refreshToken) { * * @returns {Promise>} */ -export const fetchCurrentUser = withDemoMock( - () => import('./fetchCurrentUser.mock'), - { - extract: () => ({ projectId: DEMO_PROJECT_ID }), - mapMock: (mock) => mock.default || mock, - } -)(async function fetchCurrentUser() { +export async function fetchCurrentUser() { return await api.call(QUERY_CURRENT_USER, {}, undefined, { allowErrors: true }); -}); +} /** * Update user profile @@ -173,12 +154,6 @@ export async function updateNotificationsReceiveType(payload) { * * @returns {Promise>} */ -export const fetchBankCards = withDemoMock( - () => import('./fetchBankCards.mock'), - { - extract: () => ({ projectId: DEMO_PROJECT_ID }), - mapMock: (mock) => (mock.default || mock).me.bankCards, - } -)(async function fetchBankCards() { +export async function fetchBankCards() { return (await api.callOld(QUERY_BANK_CARDS)).me.bankCards || []; -}); +} diff --git a/src/api/user/login.mock.ts b/src/api/user/login.mock.ts deleted file mode 100644 index 086876d6c..000000000 --- a/src/api/user/login.mock.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Mock tokens for demo authentication - */ -const mockTokens = { - data: { - login: { - accessToken: 'demo-access-token-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', - refreshToken: 'demo-refresh-token-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', - }, - }, - errors: [], -}; - -export default mockTokens; diff --git a/src/api/user/signUp.mock.ts b/src/api/user/signUp.mock.ts deleted file mode 100644 index 3a26ecb36..000000000 --- a/src/api/user/signUp.mock.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Mock response for sign up - */ -const mockSignUp = { - data: { - signUp: true, - }, - errors: [], -}; - -export default mockSignUp; diff --git a/src/api/workspaces/getAllWorkspacesWithProjects.mock.ts b/src/api/workspaces/getAllWorkspacesWithProjects.mock.ts index e4f3e9f11..2067c61f4 100644 --- a/src/api/workspaces/getAllWorkspacesWithProjects.mock.ts +++ b/src/api/workspaces/getAllWorkspacesWithProjects.mock.ts @@ -1,6 +1,5 @@ import type { Workspace } from '@/types/workspaces'; import { DEMO_PROJECT_ID, DEMO_WORKSPACE_ID } from '@/utils/withDemoMock'; -import { DEMO_USER } from '@/api/user/demoUser.mock'; import { DEMO_PROJECT } from './getWorkspaces.mock'; import mockDailyEventsPortion from '@/api/events/fetchDailyEventsPortion.mock'; @@ -9,48 +8,53 @@ import mockDailyEventsPortion from '@/api/events/fetchDailyEventsPortion.mock'; */ export const DEMO_WORKSPACE: Workspace = { id: DEMO_WORKSPACE_ID, - workspaceId: DEMO_WORKSPACE_ID, - name: 'Demo Workspace - Error Tracking Hub', - description: 'Comprehensive workspace for monitoring and managing application errors in real-time', - image: 'https://ui-avatars.com/api/?name=Demo+Workspace&background=FF6B6B', + name: 'Demo Workspace', + description: 'Here you can check how Hawk works', + image: 'https://static.hawk.so/fb59c4d7-db38-46d9-936e-c0d468ab1ea2.png', inviteHash: 'demo-invite-hash-abc123def456', team: [ - { - id: 'member-001-admin', - user: DEMO_USER, - isAdmin: true, - }, - { - id: 'member-002-dev', - user: { - id: 'dev-user-001', - email: 'dev@example.com', - name: 'John Developer', - image: 'https://ui-avatars.com/api/?name=John+Developer&background=4ECDC4', - }, - isAdmin: false, - }, - { - id: 'member-003-lead', - user: { - id: 'lead-user-001', - email: 'team-lead@example.com', - name: 'Sarah Team Lead', - image: 'https://ui-avatars.com/api/?name=Sarah+Lead&background=95E1D3', - }, - isAdmin: true, - }, + // { + // id: 'member-001-admin', + // user: { + // id: 'dev-user-000', + // email: 'dev@example.com', + // name: 'John Dev', + // image: 'https://ui-avatars.com/api/?name=John+Developer&background=4ECDC4', + // }, + // isAdmin: true, + // }, + // { + // id: 'member-002-dev', + // user: { + // id: 'dev-user-001', + // email: 'dev@example.com', + // name: 'John Developer', + // image: 'https://ui-avatars.com/api/?name=John+Developer&background=4ECDC4', + // }, + // isAdmin: false, + // }, + // { + // id: 'member-003-lead', + // user: { + // id: 'lead-user-001', + // email: 'team-lead@example.com', + // name: 'Sarah Team Lead', + // image: 'https://ui-avatars.com/api/?name=Sarah+Lead&background=95E1D3', + // }, + // isAdmin: true, + // }, ], plan: { id: 'pro-plan-001', - name: 'Professional', - monthlyCount: 100000, - priceMonthly: 99, - priceYearly: 990, + name: '', + monthlyCharge: 49, + monthlyChargeCurrency: 'USD', + eventsLimit: 1000000, }, lastChargeDate: new Date('2026-01-15'), isDebug: true, isBlocked: false, + }; /** diff --git a/src/api/workspaces/getWorkspaces.mock.ts b/src/api/workspaces/getWorkspaces.mock.ts index fd9f462be..4c3a11e05 100644 --- a/src/api/workspaces/getWorkspaces.mock.ts +++ b/src/api/workspaces/getWorkspaces.mock.ts @@ -1,6 +1,5 @@ import type { Project } from '@/types/project'; import { DEMO_PROJECT_ID, DEMO_WORKSPACE_ID } from '@/utils/withDemoMock'; -import { DEMO_USER } from '@/api/user/demoUser.mock'; import mockDailyEventsPortion from '@/api/events/fetchDailyEventsPortion.mock'; /** @@ -10,42 +9,28 @@ export const DEMO_PROJECT: Project = { id: DEMO_PROJECT_ID, workspaceId: DEMO_WORKSPACE_ID, token: 'token_6215743cf3ff6b80215cb183_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJpc3MiOiJodHRwczovL2hhd2suc28iLCJzdWIiOiIyMjQ2MjExODUyMjcwMzY4MzIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE2MDMzOTAwMDB9', - name: 'Production Application', - uidAdded: DEMO_USER, + name: 'Production App', + uidAdded: { + id: 'dev-user-000', + email: 'dev@example.com' + }, unreadCount: 5, description: 'Production environment - real-time error tracking and monitoring for main application', image: 'https://ui-avatars.com/api/?name=Production+App&background=FF6B6B', notifications: [ - { - id: 'notif-001', - rule: { - channels: { - email: true, - slack: true, - telegram: false, - }, - receiveType: { - onlyNew: false, - onlyAssigned: false, - }, - }, - }, ], eventGroupingPatterns: [ { id: 'pattern-001', - regexp: 'TypeError.*Cannot read.*undefined', - template: '[TypeError] Cannot read property', + pattern: 'TypeError.*Cannot read.*undefined', }, { id: 'pattern-002', - regexp: 'ReferenceError.*is not defined', - template: '[ReferenceError] Variable not defined', + pattern: 'ReferenceError.*is not defined', }, { id: 'pattern-003', - regexp: 'SyntaxError.*Unexpected token', - template: '[SyntaxError] Parser error', + pattern: 'SyntaxError.*Unexpected token', }, ], rateLimitSettings: { diff --git a/src/api/workspaces/index.ts b/src/api/workspaces/index.ts index 705c8e4e2..d54ec481a 100644 --- a/src/api/workspaces/index.ts +++ b/src/api/workspaces/index.ts @@ -61,14 +61,8 @@ export async function leaveWorkspace(workspaceId: string): Promise { * Returns all user's workspaces and project. * @returns */ -export const getAllWorkspacesWithProjects = withDemoMock( - () => import('./getAllWorkspacesWithProjects.mock'), - { - extract: () => ({ workspaceId: DEMO_WORKSPACE_ID }), - mapMock: (mock) => mock.default || mock, - } -)(async function getAllWorkspacesWithProjects(): Promise> { - return api.call(QUERY_ALL_WORKSPACES_WITH_PROJECTS, undefined, undefined, { +export async function getAllWorkspacesWithProjects(): Promise> { + const response = await api.call(QUERY_ALL_WORKSPACES_WITH_PROJECTS, undefined, undefined, { initial: true, /** @@ -77,7 +71,20 @@ export const getAllWorkspacesWithProjects = withDemoMock( */ allowErrors: true, }); -}); + + // Remove Demo workspace from the response if it is present, since we will add it with enriched data later + response.data.workspaces = response.data.workspaces.filter((ws, index) => { + return ws.id !== DEMO_WORKSPACE_ID; + }); + + // add demo at the start of the list if there is no such workspace in the response + response.data.workspaces = [ + (await import('./getAllWorkspacesWithProjects.mock').then((m) => m.default)).data.workspaces[0], + ...response.data.workspaces, + ]; + + return response; +} /** * Invites user to workspace by email @@ -120,15 +127,9 @@ export async function confirmInvite(workspaceId: string, inviteHash: string): Pr * @param ids – id of fetching workspaces * @returns */ -export const getWorkspaces = withDemoMock( - () => import('./getWorkspaces.mock'), - { - extract: (args) => ({ workspaceId: args[0]?.[0] }), - mapMock: (mock) => ({ workspaces: (mock.default || mock).data.workspaces }), - } -)(async function getWorkspaces(ids: string[]): Promise { +export async function getWorkspaces(ids: string[]): Promise { return (await api.callOld(QUERY_WORKSPACES, { ids })).workspaces; -}); +} /** * Get workspace balance From e2bb13891cb54da47c8275f18640248e9b92bbae Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 7 Feb 2026 13:16:56 +0300 Subject: [PATCH 03/44] Update .nvmrc --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index a682cfb97..6ac2314c3 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v25 +v24.11.1 \ No newline at end of file From a4bea5bb86aecd0428b898ef6fbbb5d866c6af47 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 7 Feb 2026 13:16:58 +0300 Subject: [PATCH 04/44] Update main.ts --- src/main.ts | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/main.ts b/src/main.ts index 1929e6c71..0a92fe421 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,7 +10,6 @@ import { i18n } from './i18n'; import * as api from './api/index'; import { REFRESH_TOKENS } from './store/modules/user/actionTypes'; import { RESET_STORE } from './store/methodsTypes'; -import { enableDemoMode } from './utils/withDemoMock'; import '@codexteam/ui/styles'; @@ -29,17 +28,6 @@ import setupDirectives from './directives'; const { init: initHawk, track } = useErrorTracker(); -/** - * Enable demo mode if running without a server - * You can disable this in production or enable via console: enableDemoMode() - */ -console.log('[App Init] VITE_API_URL:', import.meta.env.VITE_API_URL); -if (!import.meta.env.VITE_API_URL || import.meta.env.VITE_API_URL === 'http://localhost:4000') { - enableDemoMode(); -} else { - console.log('[App Init] Using real API server at:', import.meta.env.VITE_API_URL); -} - const app = createApp(App); app.use(router); @@ -130,14 +118,4 @@ api.setupApiModuleHandlers({ time: 5000, }); }, DEBOUNCE_TIMEOUT), -}); - -/** - * Export demo mode functions to window for browser console usage - * Usage: enableDemoMode() or disableDemoMode() in browser console - */ -(window as any).enableDemoMode = enableDemoMode; -(window as any).disableDemoMode = () => { - const { disableDemoMode } = require('./utils/withDemoMock'); - disableDemoMode(); -}; +}); \ No newline at end of file From bd626b1974a49978145e69180add73bb748ebe37 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 7 Feb 2026 13:18:58 +0300 Subject: [PATCH 05/44] Update EventsList.vue --- src/components/project/EventsList.vue | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/src/components/project/EventsList.vue b/src/components/project/EventsList.vue index a1d650351..568c57238 100644 --- a/src/components/project/EventsList.vue +++ b/src/components/project/EventsList.vue @@ -19,7 +19,7 @@ { - console.log(` ✅ [${i}] eventId=${de.eventId}`); - }); - this.dailyEventsNextCursor = nextCursor; this.noMore = this.dailyEventsNextCursor === null; @@ -313,8 +299,6 @@ export default { this.dailyEvents.push(...dailyEventsWithEventsLinked); } - console.log('[✅ loadMoreEvents] After push: dailyEvents.length=', this.dailyEvents.length); - this.isLoading = false; }, /** @@ -366,11 +350,6 @@ export default { onShowEventOverview(eventId) { const event = this.getEvent(eventId); - if (!event) { - console.error('[EventsList] onShowEventOverview: event not found for eventId:', eventId); - return; - } - const originalEventId = event.originalEventId; if (this.isAssigneesShowed) { From 34e71936366347d0b49cfcd766c4f4ddbfb2a556 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 7 Feb 2026 13:20:40 +0300 Subject: [PATCH 06/44] Update index.ts --- src/store/modules/events/index.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/store/modules/events/index.ts b/src/store/modules/events/index.ts index 18c785ba3..7dca85c6b 100644 --- a/src/store/modules/events/index.ts +++ b/src/store/modules/events/index.ts @@ -179,16 +179,8 @@ const module: Module = { */ return (projectId: string, eventId: string): HawkEvent | null => { const key = getEventsListKey(projectId, eventId); - const event = state.events[key] || null; - if (!event) { - const availableKeys = Object.keys(state.events).filter(k => k.startsWith(projectId + ':')); - console.warn(`[⚠️ getProjectEventById] NOT FOUND: key="${key}". Available keys for ${projectId}:`, availableKeys); - } else { - console.log(`[✅ getProjectEventById] FOUND: ${key}`); - } - - return event; + return state.events[key] || null; }; }, From c10df9efa019bc78b7165623cd6e6c26004b6b01 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 7 Feb 2026 13:22:15 +0300 Subject: [PATCH 07/44] Update .nvmrc --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index 6ac2314c3..8ef0a5258 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v24.11.1 \ No newline at end of file +v24.11.1 From 2d10b0788bf45feb9d60592872255d4bc4b558fc Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 7 Feb 2026 13:22:17 +0300 Subject: [PATCH 08/44] Update EventsList.vue --- src/components/project/EventsList.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/project/EventsList.vue b/src/components/project/EventsList.vue index 568c57238..5a53baf0e 100644 --- a/src/components/project/EventsList.vue +++ b/src/components/project/EventsList.vue @@ -349,7 +349,6 @@ export default { */ onShowEventOverview(eventId) { const event = this.getEvent(eventId); - const originalEventId = event.originalEventId; if (this.isAssigneesShowed) { From 445b011d31d4446944bb97dd2733388d27d03579 Mon Sep 17 00:00:00 2001 From: Taly Date: Sat, 7 Feb 2026 13:22:19 +0300 Subject: [PATCH 09/44] Update main.ts --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 0a92fe421..baf651043 100644 --- a/src/main.ts +++ b/src/main.ts @@ -118,4 +118,4 @@ api.setupApiModuleHandlers({ time: 5000, }); }, DEBOUNCE_TIMEOUT), -}); \ No newline at end of file +}); From 2be3421597a1ef565b71ceb2a771fb3849d8ccea Mon Sep 17 00:00:00 2001 From: Taly Date: Wed, 11 Feb 2026 18:49:26 +0300 Subject: [PATCH 10/44] Add demo mode, mock DB and API mocks Introduce a demo mode backed by a mock database and organized mock API modules. Added a demo Vuex module (store/modules/demo), wired demo state into the root store and persisted state, and updated the router to support /demo paths and set demo tokens. Replaced the old withDemoMock utility with withMockDemo and updated API wrappers (events, projects, workspaces, user) to load mocks from new ./mocks/* locations. Created a central mock-db (users, workspaces, events) with demo data and multiple new mock responses for charts, releases, project visits, and current user. UI changes include a demo button on the login page and i18n strings for demo mode. Removed legacy mock files and simplified mock loading logic. --- src/api/events/fetchChartData.mock.ts | 37 ---- src/api/events/index.ts | 45 ++--- src/api/events/mocks/fetchChartData.mock.ts | 78 ++++++++ .../fetchDailyEventsPortion.mock.ts | 0 src/api/events/{ => mocks}/getEvent.mock.ts | 0 .../{ => mocks}/getRepetitionsPortion.mock.ts | 0 .../{ => mocks}/toggleEventMark.mock.ts | 0 src/api/events/{ => mocks}/visitEvent.mock.ts | 0 src/api/mock-db/events.ts | 156 +++++++++++++++ src/api/mock-db/index.ts | 9 + src/api/mock-db/users.ts | 35 ++++ src/api/mock-db/workspaces.ts | 108 +++++++++++ src/api/projects/fetchChartData.mock.js | 47 ----- .../fetchProjectReleaseDetails.mock.ts | 74 ------- src/api/projects/fetchProjectReleases.mock.ts | 66 ------- src/api/projects/getProjects.mock.ts | 8 - src/api/projects/index.js | 32 +-- src/api/projects/mocks/fetchChartData.mock.ts | 15 ++ .../mocks/fetchProjectReleaseDetails.mock.ts | 47 +++++ .../mocks/fetchProjectReleases.mock.ts | 39 ++++ .../mocks/updateLastProjectVisit.mock.ts | 8 + src/api/user/index.js | 7 +- src/api/user/mocks/fetchCurrentUser.mock.ts | 16 ++ .../getAllWorkspacesWithProjects.mock.ts | 81 -------- src/api/workspaces/getWorkspaces.mock.ts | 65 ------- src/api/workspaces/index.ts | 23 +-- .../getAllWorkspacesWithProjects.mock.ts | 47 +++++ src/components/auth/Login.vue | 34 +++- src/i18n/messages/en.json | 4 + src/i18n/messages/ru.json | 7 +- src/router.ts | 25 +++ src/store/index.ts | 4 + src/store/modules/demo/index.ts | 94 +++++++++ src/utils/withDemoMock.ts | 182 ------------------ src/utils/withMockDemo.ts | 108 +++++++++++ 35 files changed, 864 insertions(+), 637 deletions(-) delete mode 100644 src/api/events/fetchChartData.mock.ts create mode 100644 src/api/events/mocks/fetchChartData.mock.ts rename src/api/events/{ => mocks}/fetchDailyEventsPortion.mock.ts (100%) rename src/api/events/{ => mocks}/getEvent.mock.ts (100%) rename src/api/events/{ => mocks}/getRepetitionsPortion.mock.ts (100%) rename src/api/events/{ => mocks}/toggleEventMark.mock.ts (100%) rename src/api/events/{ => mocks}/visitEvent.mock.ts (100%) create mode 100644 src/api/mock-db/events.ts create mode 100644 src/api/mock-db/index.ts create mode 100644 src/api/mock-db/users.ts create mode 100644 src/api/mock-db/workspaces.ts delete mode 100644 src/api/projects/fetchChartData.mock.js delete mode 100644 src/api/projects/fetchProjectReleaseDetails.mock.ts delete mode 100644 src/api/projects/fetchProjectReleases.mock.ts delete mode 100644 src/api/projects/getProjects.mock.ts create mode 100644 src/api/projects/mocks/fetchChartData.mock.ts create mode 100644 src/api/projects/mocks/fetchProjectReleaseDetails.mock.ts create mode 100644 src/api/projects/mocks/fetchProjectReleases.mock.ts create mode 100644 src/api/projects/mocks/updateLastProjectVisit.mock.ts create mode 100644 src/api/user/mocks/fetchCurrentUser.mock.ts delete mode 100644 src/api/workspaces/getAllWorkspacesWithProjects.mock.ts delete mode 100644 src/api/workspaces/getWorkspaces.mock.ts create mode 100644 src/api/workspaces/mocks/getAllWorkspacesWithProjects.mock.ts create mode 100644 src/store/modules/demo/index.ts delete mode 100644 src/utils/withDemoMock.ts create mode 100644 src/utils/withMockDemo.ts diff --git a/src/api/events/fetchChartData.mock.ts b/src/api/events/fetchChartData.mock.ts deleted file mode 100644 index 470aec8a9..000000000 --- a/src/api/events/fetchChartData.mock.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { ChartLine } from '@/types/chart'; - -/** - * Mock data for fetchChartData - */ -const mockFetchChartData: ChartLine[] = [ - { - date: '2026-01-28', - count: 42, - }, - { - date: '2026-01-27', - count: 35, - }, - { - date: '2026-01-26', - count: 28, - }, - { - date: '2026-01-25', - count: 31, - }, - { - date: '2026-01-24', - count: 25, - }, - { - date: '2026-01-23', - count: 18, - }, - { - date: '2026-01-22', - count: 22, - }, -]; - -export default mockFetchChartData; diff --git a/src/api/events/index.ts b/src/api/events/index.ts index 1a7ad980d..7b0189504 100644 --- a/src/api/events/index.ts +++ b/src/api/events/index.ts @@ -22,7 +22,7 @@ import { import type { User } from '@/types/user'; import type { EventChartItem, ChartLine } from '@/types/chart'; import type { APIResponse } from '../../types/api'; -import { withDemoMock, DEMO_PROJECT_ID } from '@/utils/withDemoMock'; +import { withMockDemo } from '@/utils/withMockDemo'; /** * Get specific event @@ -31,11 +31,8 @@ import { withDemoMock, DEMO_PROJECT_ID } from '@/utils/withDemoMock'; * @param originalEventId - id of the original event * @returns */ -export const getEvent = withDemoMock( - () => import('./getEvent.mock').then(m => m.default), - { - extract: (args) => ({ projectId: args[0] }), - } +export const getEvent = withMockDemo( + () => import('./mocks/getEvent.mock').then(m => m.default) )(async function getEvent(projectId: string, eventId: string, originalEventId: string): Promise { const project = await (await api.callOld(QUERY_EVENT, { projectId, @@ -59,11 +56,8 @@ export const getEvent = withDemoMock( * @param search - search string for daily events * @param release - release identifier to filter events */ -export const fetchDailyEventsPortion = withDemoMock( - () => import('./fetchDailyEventsPortion.mock').then(m => m.default), - { - extract: (args) => ({ projectId: args[0] }), - } +export const fetchDailyEventsPortion = withMockDemo( + () => import('./mocks/fetchDailyEventsPortion.mock').then(m => m.default) )(async function fetchDailyEventsPortion( projectId: string, nextCursor: DailyEventsCursor | null = null, @@ -105,12 +99,8 @@ export const fetchDailyEventsPortion = withDemoMock( * @param cursor - the cursor to fetch the next page of repetitions * @returns */ -export const getRepetitionsPortion = withDemoMock( - () => import('./getRepetitionsPortion.mock').then(m => m.default), - { - extract: (args) => ({ projectId: args[0] }), - mapMock: (mock) => mock.default || mock, - } +export const getRepetitionsPortion = withMockDemo( + () => import('./mocks/getRepetitionsPortion.mock').then(m => m.default) )(async function getRepetitionsPortion( projectId: string, originalEventId: string, limit: number, cursor?: string ): Promise import('./visitEvent.mock').then(m => m.default), - { - extract: (args) => ({ projectId: args[0] }), - } +export const visitEvent = withMockDemo( + () => import('./mocks/visitEvent.mock').then(m => m.default) )(async function visitEvent(projectId: string, originalEventId: string): Promise { return (await api.callOld(MUTATION_VISIT_EVENT, { projectId, @@ -159,11 +146,8 @@ export const visitEvent = withDemoMock( * @param eventId — event Id * @param mark — mark to set */ -export const toggleEventMark = withDemoMock( - () => import('./toggleEventMark.mock').then(m => m.default), - { - extract: (args) => ({ projectId: args[0] }), - } +export const toggleEventMark = withMockDemo( + () => import('./mocks/toggleEventMark.mock').then(m => m.default) )(async function toggleEventMark(projectId: string, eventId: string, mark: EventMark): Promise { return (await api.callOld(MUTATION_TOGGLE_EVENT_MARK, { projectId, @@ -210,11 +194,8 @@ export async function removeAssignee(projectId: string, eventId: string): Promis * @param days - how many days we need to fetch for displaying in chart * @param timezoneOffset - user's local timezone */ -export const fetchChartData = withDemoMock( - () => import('./fetchChartData.mock').then(m => m.default), - { - extract: (args) => ({ projectId: args[0] }), - } +export const fetchChartData = withMockDemo( + () => import('./mocks/fetchChartData.mock').then(m => m.default) )(async function fetchChartData( projectId: string, originalEventId: string, diff --git a/src/api/events/mocks/fetchChartData.mock.ts b/src/api/events/mocks/fetchChartData.mock.ts new file mode 100644 index 000000000..4e1fbebbc --- /dev/null +++ b/src/api/events/mocks/fetchChartData.mock.ts @@ -0,0 +1,78 @@ +import type { ChartLine } from '@/types/chart'; + +const DAY = 86400; +const NOW = Math.floor(Date.now() / 1000); + +/** + * Mock data for fetchChartData + */ +const mockFetchChartData: ChartLine[] = [ + { + label: 'accepted', + data: [ + { + timestamp: NOW - 6 * DAY, + count: 175, + }, + { + timestamp: NOW - 5 * DAY, + count: 242, + }, + { + timestamp: NOW - 4 * DAY, + count: 198, + }, + { + timestamp: NOW - 3 * DAY, + count: 215, + }, + { + timestamp: NOW - 2 * DAY, + count: 321, + }, + { + timestamp: NOW - 1 * DAY, + count: 298, + }, + { + timestamp: NOW, + count: 335, + }, + ], + }, + { + label: 'rate-limited', + data: [ + { + timestamp: NOW - 6 * DAY, + count: 105, + }, + { + timestamp: NOW - 5 * DAY, + count: 122, + }, + { + timestamp: NOW - 4 * DAY, + count: 18, + }, + { + timestamp: NOW - 3 * DAY, + count: 2500, + }, + { + timestamp: NOW - 2 * DAY, + count: 31, + }, + { + timestamp: NOW - 1 * DAY, + count: 128, + }, + { + timestamp: NOW, + count: 135, + }, + ] + } +]; + +export default mockFetchChartData; diff --git a/src/api/events/fetchDailyEventsPortion.mock.ts b/src/api/events/mocks/fetchDailyEventsPortion.mock.ts similarity index 100% rename from src/api/events/fetchDailyEventsPortion.mock.ts rename to src/api/events/mocks/fetchDailyEventsPortion.mock.ts diff --git a/src/api/events/getEvent.mock.ts b/src/api/events/mocks/getEvent.mock.ts similarity index 100% rename from src/api/events/getEvent.mock.ts rename to src/api/events/mocks/getEvent.mock.ts diff --git a/src/api/events/getRepetitionsPortion.mock.ts b/src/api/events/mocks/getRepetitionsPortion.mock.ts similarity index 100% rename from src/api/events/getRepetitionsPortion.mock.ts rename to src/api/events/mocks/getRepetitionsPortion.mock.ts diff --git a/src/api/events/toggleEventMark.mock.ts b/src/api/events/mocks/toggleEventMark.mock.ts similarity index 100% rename from src/api/events/toggleEventMark.mock.ts rename to src/api/events/mocks/toggleEventMark.mock.ts diff --git a/src/api/events/visitEvent.mock.ts b/src/api/events/mocks/visitEvent.mock.ts similarity index 100% rename from src/api/events/visitEvent.mock.ts rename to src/api/events/mocks/visitEvent.mock.ts diff --git a/src/api/mock-db/events.ts b/src/api/mock-db/events.ts new file mode 100644 index 000000000..33f92d074 --- /dev/null +++ b/src/api/mock-db/events.ts @@ -0,0 +1,156 @@ +/** + * Mock database: Events + * + * Contains demo error events with realistic data + */ + +import type { HawkEvent } from '@/types/events'; +import { DEMO_USER } from './users'; +import { DEMO_PROJECT_ID } from './workspaces'; + +/** + * Helper to create realistic error event + */ +function createDemoEvent(config: { + id: string; + originalEventId: string; + title: string; + type: string; + groupHash: string; + totalCount: number; + usersAffected: number; + timestamp?: number; + file?: string; + line?: number; + isStarred?: boolean; +}): HawkEvent { + const { + id, + originalEventId, + title, + type, + groupHash, + totalCount, + usersAffected, + timestamp = Date.now(), + file = 'src/store/user.ts', + line = 42, + isStarred = false, + } = config; + + return { + id, + groupHash, + totalCount, + usersAffected, + visitedBy: [DEMO_USER], + marks: { + resolved: false, + starred: isStarred, + ignored: false, + }, + payload: { + title, + type, + backtrace: [ + { + file, + line, + column: 15, + function: 'getUserProfile', + arguments: ['userId'], + sourceCode: [ + { line: line - 2, content: 'export const getUserProfile = (userId) => {' }, + { line: line - 1, content: ' const user = store.getters.user;' }, + { line, content: ' return user.profile.settings;' }, + { line: line + 1, content: '};' }, + ], + }, + ], + get: { + userId: '507f1f77bcf86cd799439011', + format: 'json', + }, + post: {}, + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', + 'Accept-Language': 'en-US,en;q=0.9', + }, + release: 'v1.2.3', + user: { + id: 'user-demo-001', + } as any, + context: { + browser: 'Chrome 120.0.0', + os: 'macOS 14.2.1', + screen: '1920x1080', + timezone: 'UTC+2', + language: 'en-US', + url: `http://localhost:8080/project/${DEMO_PROJECT_ID}`, + }, + addons: {} as any, + }, + catcherType: 'client/javascript', + repetitions: [], + assignee: undefined as any, + timestamp, + originalTimestamp: timestamp - 86400000, // 1 day ago + originalEventId, + }; +} + +/** + * Demo events collection + */ +export const DEMO_EVENTS: HawkEvent[] = [ + createDemoEvent({ + id: '507f1f77bcf86cd799439011', + originalEventId: '507f1f77bcf86cd799439010', + title: 'TypeError: Cannot read property \'user\' of undefined', + type: 'TypeError', + groupHash: 'hash-507f1f77bcf86cd799439011', + totalCount: 42, + usersAffected: 8, + isStarred: true, + }), + createDemoEvent({ + id: '507f191e810c19729de860ea', + originalEventId: '507f191e810c19729de860e9', + title: 'ReferenceError: apiKey is not defined', + type: 'ReferenceError', + groupHash: 'hash-507f191e810c19729de860ea', + totalCount: 18, + usersAffected: 5, + file: 'src/api/config.ts', + line: 15, + }), + createDemoEvent({ + id: '507f1f77bcf86cd799439012', + originalEventId: '507f1f77bcf86cd799439013', + title: 'Network Error: Failed to fetch user data', + type: 'NetworkError', + groupHash: 'hash-507f1f77bcf86cd799439012', + totalCount: 7, + usersAffected: 3, + file: 'src/api/user/index.ts', + line: 28, + }), + createDemoEvent({ + id: '507f1f77bcf86cd799439014', + originalEventId: '507f1f77bcf86cd799439015', + title: 'SyntaxError: Unexpected token < in JSON', + type: 'SyntaxError', + groupHash: 'hash-507f1f77bcf86cd799439014', + totalCount: 12, + usersAffected: 4, + file: 'src/utils/parser.ts', + line: 55, + }), +]; + +/** + * Get event by ID + */ +export function getDemoEventById(id: string): HawkEvent | undefined { + return DEMO_EVENTS.find(event => event.id === id); +} diff --git a/src/api/mock-db/index.ts b/src/api/mock-db/index.ts new file mode 100644 index 000000000..823dcf9b0 --- /dev/null +++ b/src/api/mock-db/index.ts @@ -0,0 +1,9 @@ +/** + * Mock Database - Central Export + * + * Re-exports all mock data from subdirectories + */ + +export * from './users'; +export * from './workspaces'; +export * from './events'; diff --git a/src/api/mock-db/users.ts b/src/api/mock-db/users.ts new file mode 100644 index 000000000..0014a9b37 --- /dev/null +++ b/src/api/mock-db/users.ts @@ -0,0 +1,35 @@ +/** + * Mock database: Users + * + * Contains demo user data + */ + +import type { User } from '@/types/user'; + +/** + * Demo user account + */ +export const DEMO_USER: User = { + id: 'user-demo-001', + email: 'demo@hawk.so', + name: 'Demo User', + image: 'https://ui-avatars.com/api/?name=John+Dev&background=4ECDC4&color=fff', +}; + +/** + * Additional team members for demo workspace + */ +export const DEMO_TEAM_MEMBERS: User[] = [ + { + id: 'user-dev-001', + email: 'john@example.com', + name: 'John Developer', + image: 'https://ui-avatars.com/api/?name=John+Dev&background=4ECDC4&color=fff', + }, + { + id: 'user-lead-001', + email: 'sarah@example.com', + name: 'Sarah Team Lead', + image: 'https://ui-avatars.com/api/?name=Sarah+Lead&background=95E1D3&color=fff', + }, +]; diff --git a/src/api/mock-db/workspaces.ts b/src/api/mock-db/workspaces.ts new file mode 100644 index 000000000..42acc0a93 --- /dev/null +++ b/src/api/mock-db/workspaces.ts @@ -0,0 +1,108 @@ +/** + * Mock database: Workspaces & Projects + * + * Contains demo workspace and project data with proper references + */ + +import type { Workspace } from '@/types/workspaces'; +import type { Project } from '@/types/project'; +import { ReceiveTypes } from '@/types/project-notifications'; +import { DEMO_USER, DEMO_TEAM_MEMBERS } from './users'; + +/** + * Demo workspace ID (used across the app) + */ +export const DEMO_WORKSPACE_ID = '6213b6a01e6281087467cc7a'; + +/** + * Demo project ID (used across the app) + */ +export const DEMO_PROJECT_ID = '6215743cf3ff6b80215cb183'; + +/** + * Demo workspace with team and plan + */ +export const DEMO_WORKSPACE: Workspace = { + id: DEMO_WORKSPACE_ID, + name: 'Demo Workspace', + description: 'This is a demo workspace showcasing Hawk error tracking', + image: 'https://static.hawk.so/fb59c4d7-db38-46d9-936e-c0d468ab1ea2.png', + inviteHash: 'demo-invite-hash', + team: [ + { + id: 'member-demo-001', + user: DEMO_USER, + isAdmin: true, + }, + { + id: 'member-dev-001', + user: DEMO_TEAM_MEMBERS[0], + isAdmin: false, + }, + { + id: 'member-lead-001', + user: DEMO_TEAM_MEMBERS[1], + isAdmin: true, + }, + ], + plan: { + id: 'plan-pro-001', + name: 'Professional', + monthlyCharge: 99, + monthlyChargeCurrency: 'USD', + eventsLimit: 100000, + }, + lastChargeDate: new Date('2026-01-15'), + isDebug: true, + isBlocked: false, +}; + +/** + * Demo project within workspace + */ +export const DEMO_PROJECT: Project = { + id: DEMO_PROJECT_ID, + workspaceId: DEMO_WORKSPACE_ID, + token: `hawk_${DEMO_PROJECT_ID}_demo_token`, + name: 'Production App', + uidAdded: DEMO_USER, + unreadCount: 5, + description: 'Production environment error tracker', + image: 'https://ui-avatars.com/api/?name=Prod+App&background=4ECDC4&color=fff', + notifications: [ + { + id: 'notif-001', + uidAdded: DEMO_USER.id, + channels: { + email: { + endpoint: 'demo@hawk.so', + isEnabled: true, + }, + slack: { + endpoint: 'https://hooks.slack.com/demo', + isEnabled: true, + }, + telegram: { + endpoint: '', + isEnabled: false, + }, + }, + whatToReceive: ReceiveTypes.SEEN_MORE, + isEnabled: true, + }, + ], + eventGroupingPatterns: [ + { + id: 'pattern-001', + pattern: 'TypeError.*Cannot read.*undefined', + }, + { + id: 'pattern-002', + pattern: 'ReferenceError.*is not defined', + }, + ], + rateLimitSettings: { + N: 100000, + T: 86400, + }, +}; diff --git a/src/api/projects/fetchChartData.mock.js b/src/api/projects/fetchChartData.mock.js deleted file mode 100644 index 8596445a8..000000000 --- a/src/api/projects/fetchChartData.mock.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Mock data for project overview chart - */ -const mockFetchChartData = { - data: { - project: { - chartData: [ - { - label: 'Events', - data: [ - { - timestamp: Date.now() - 6 * 86400000, - count: 15, - }, - { - timestamp: Date.now() - 5 * 86400000, - count: 22, - }, - { - timestamp: Date.now() - 4 * 86400000, - count: 18, - }, - { - timestamp: Date.now() - 3 * 86400000, - count: 25, - }, - { - timestamp: Date.now() - 2 * 86400000, - count: 31, - }, - { - timestamp: Date.now() - 1 * 86400000, - count: 28, - }, - { - timestamp: Date.now(), - count: 35, - }, - ], - }, - ], - }, - }, - errors: [], -}; - -export default mockFetchChartData; diff --git a/src/api/projects/fetchProjectReleaseDetails.mock.ts b/src/api/projects/fetchProjectReleaseDetails.mock.ts deleted file mode 100644 index 99df956f3..000000000 --- a/src/api/projects/fetchProjectReleaseDetails.mock.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Mock data for fetchProjectReleaseDetails - */ -const mockFetchProjectReleaseDetails = { - data: { - project: { - releaseDetails: { - id: 'release-rel-001', - release: 'v1.2.3', - version: '1.2.3', - created: new Date('2026-01-28'), - description: 'Latest stable release with bug fixes and performance improvements', - commits: 24, - filesChanged: 156, - insertions: 2841, - deletions: 1205, - author: 'John Developer', - authorEmail: 'dev@example.com', - changelog: ` -## v1.2.3 - 2026-01-28 - -### New Features -- Added event filtering by browser type -- Implemented real-time notifications for critical errors -- New dashboard widget for error trends - -### Bug Fixes -- Fixed crash when loading events without timezone data -- Resolved memory leak in event storage -- Fixed typo in notification templates - -### Performance -- Optimized database queries for large datasets -- Reduced bundle size by 15% -- Improved chart rendering performance - -### Breaking Changes -- Removed deprecated API endpoints from v1.1.x -- Updated minimum Node.js version to 18.0.0 - -### Contributors -- John Developer (@johndev) -- Sarah Team Lead (@sarahteam) -- Alex Frontend (@alexfrontend) - `, - eventGroupingPatterns: [ - { - id: 'pattern-001', - regexp: 'TypeError.*Cannot read.*undefined', - template: '[TypeError] Cannot read property', - }, - { - id: 'pattern-002', - regexp: 'ReferenceError.*is not defined', - template: '[ReferenceError] Variable not defined', - }, - { - id: 'pattern-003', - regexp: 'SyntaxError.*Unexpected token', - template: '[SyntaxError] Parser error', - }, - { - id: 'pattern-004', - regexp: 'NetworkError.*Failed to fetch', - template: '[NetworkError] Request failed', - }, - ], - }, - }, - }, - errors: [], -}; - -export default mockFetchProjectReleaseDetails; diff --git a/src/api/projects/fetchProjectReleases.mock.ts b/src/api/projects/fetchProjectReleases.mock.ts deleted file mode 100644 index 945967bb7..000000000 --- a/src/api/projects/fetchProjectReleases.mock.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Mock data for fetchProjectReleases - */ -const mockFetchProjectReleases = { - data: { - project: { - releases: [ - { - id: 'release-rel-001', - release: 'v1.2.3', - version: '1.2.3', - created: new Date('2026-01-28'), - description: 'Latest stable release with bug fixes and performance improvements', - eventGroupingPatterns: [], - commits: 24, - filesChanged: 156, - insertions: 2841, - deletions: 1205, - author: 'John Developer', - }, - { - id: 'release-rel-002', - release: 'v1.2.2', - version: '1.2.2', - created: new Date('2026-01-21'), - description: 'Hotfix release - critical security patches', - eventGroupingPatterns: [], - commits: 5, - filesChanged: 8, - insertions: 142, - deletions: 89, - author: 'Sarah Team Lead', - }, - { - id: 'release-rel-003', - release: 'v1.2.1', - version: '1.2.1', - created: new Date('2026-01-14'), - description: 'Minor release - new features and improvements', - eventGroupingPatterns: [], - commits: 38, - filesChanged: 203, - insertions: 4156, - deletions: 1876, - author: 'John Developer', - }, - { - id: 'release-rel-004', - release: 'v1.2.0', - version: '1.2.0', - created: new Date('2026-01-07'), - description: 'Major release - new API endpoints and database schema', - eventGroupingPatterns: [], - commits: 87, - filesChanged: 451, - insertions: 12456, - deletions: 5643, - author: 'Sarah Team Lead', - }, - ], - }, - }, - errors: [], -}; - -export default mockFetchProjectReleases; diff --git a/src/api/projects/getProjects.mock.ts b/src/api/projects/getProjects.mock.ts deleted file mode 100644 index c9c68d227..000000000 --- a/src/api/projects/getProjects.mock.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { DEMO_PROJECT } from './getWorkspaces.mock'; - -/** - * Mock for projects list in workspace - */ -const mockProjectsList = [DEMO_PROJECT]; - -export default mockProjectsList; diff --git a/src/api/projects/index.js b/src/api/projects/index.js index fb2067b95..b8f23d52e 100644 --- a/src/api/projects/index.js +++ b/src/api/projects/index.js @@ -20,7 +20,7 @@ import { MUTATION_UPDATE_TASK_MANAGER_SETTINGS } from './queries'; import * as api from '../index.ts'; -import { withDemoMock, DEMO_PROJECT_ID } from '@/utils/withDemoMock'; +import { withMockDemo } from '@/utils/withMockDemo.ts'; /** * Create project and returns its id @@ -135,11 +135,8 @@ export async function removeProject(projectId) { * @param {string} projectId - id of the project * @returns {Promise} - success status */ -export const updateLastProjectVisit = withDemoMock( - () => Promise.resolve(true), - { - extract: (args) => ({ projectId: args[0] }), - } +export const updateLastProjectVisit = withMockDemo( + () => import('./mocks/updateLastProjectVisit.mock').then(m => m.default) )(async function updateLastProjectVisit(projectId) { return (await api.callOld(MUTATION_UPDATE_LAST_VISIT, { projectId })).setLastProjectVisit; }); @@ -260,11 +257,8 @@ export async function toggleEnabledStateOfProjectNotificationsRule(payload) { * @param {number} timezoneOffset - timezone offset in minutes * @returns {Promise} - chart data response */ -export const fetchChartData = withDemoMock( - () => import('./fetchChartData.mock').then(m => m.default), - { - extract: (args) => ({ projectId: args[0] }), - } +export const fetchChartData = withMockDemo( + () => import('./mocks/fetchChartData.mock').then(m => m.default) )(async function fetchChartData(projectId, startDate, endDate, groupBy, timezoneOffset) { const response = await api.call(QUERY_CHART_DATA, { projectId, @@ -288,12 +282,8 @@ export const fetchChartData = withDemoMock( * @param {string} projectId - id of the project to fetch releases * @returns {Promise>} - list of releases with unique events count, commits count and files count */ -export const fetchProjectReleases = withDemoMock( - () => import('./fetchProjectReleases.mock'), - { - extract: (args) => ({ projectId: args[0] }), - mapMock: (mock) => (mock.default || mock).data.project.releases, - } +export const fetchProjectReleases = withMockDemo( + () => import('./mocks/fetchProjectReleases.mock').then(m => m.default) )(async function fetchProjectReleases(projectId) { const response = await api.call(QUERY_PROJECT_RELEASES, { projectId }); @@ -311,12 +301,8 @@ export const fetchProjectReleases = withDemoMock( * @param {string} release * @returns {Promise} */ -export const fetchProjectReleaseDetails = withDemoMock( - () => import('./fetchProjectReleaseDetails.mock'), - { - extract: (args) => ({ projectId: args[0] }), - mapMock: (mock) => (mock.default || mock).data.project.releaseDetails, - } +export const fetchProjectReleaseDetails = withMockDemo( + () => import('./mocks/fetchProjectReleaseDetails.mock').then(m => m.default) )(async function fetchProjectReleaseDetails(projectId, release) { const response = await api.call(QUERY_PROJECT_RELEASE_DETAILS, { projectId, release }); diff --git a/src/api/projects/mocks/fetchChartData.mock.ts b/src/api/projects/mocks/fetchChartData.mock.ts new file mode 100644 index 000000000..b8781b85d --- /dev/null +++ b/src/api/projects/mocks/fetchChartData.mock.ts @@ -0,0 +1,15 @@ +import mockChartData from "../../events/mocks/fetchChartData.mock"; + +/** + * Mock data for project overview chart + */ +const mockFetchChartData = { + data: { + project: { + chartData: mockChartData + }, + }, + errors: [], +}; + +export default mockFetchChartData; diff --git a/src/api/projects/mocks/fetchProjectReleaseDetails.mock.ts b/src/api/projects/mocks/fetchProjectReleaseDetails.mock.ts new file mode 100644 index 000000000..841190934 --- /dev/null +++ b/src/api/projects/mocks/fetchProjectReleaseDetails.mock.ts @@ -0,0 +1,47 @@ +/** + * Mock data for fetchProjectReleaseDetails + * Returns detailed information about a specific release + */ + +const releaseDetailsMock = { + release: 'v2.5.0', + timestamp: Date.now() - 86400000, + commits: [ + { + hash: 'abc123def', + message: 'Fix critical bug in payment processing', + author: 'Demo Developer', + timestamp: Date.now() - 86400000, + }, + { + hash: 'def456ghi', + message: 'Update dependencies', + author: 'Demo Developer', + timestamp: Date.now() - 86400000 - 3600000, + }, + ], + files: [ + { + path: 'src/payment/processor.ts', + additions: 15, + deletions: 8, + }, + { + path: 'package.json', + additions: 3, + deletions: 3, + }, + ], + newEventsCount: 3, +}; + +const mockFetchProjectReleaseDetails = { + data: { + project: { + releaseDetails: releaseDetailsMock, + }, + }, + errors: [], +}; + +export default mockFetchProjectReleaseDetails; diff --git a/src/api/projects/mocks/fetchProjectReleases.mock.ts b/src/api/projects/mocks/fetchProjectReleases.mock.ts new file mode 100644 index 000000000..f76580b12 --- /dev/null +++ b/src/api/projects/mocks/fetchProjectReleases.mock.ts @@ -0,0 +1,39 @@ +/** + * Mock data for fetchProjectReleases + * Returns a list of releases with event counts + */ + +const releasesMock = [ + { + release: 'v2.5.0', + timestamp: Date.now() - 86400000, // 1 day ago + newEventsCount: 3, + commitsCount: 12, + filesCount: 8, + }, + { + release: 'v2.4.1', + timestamp: Date.now() - 86400000 * 7, // 1 week ago + newEventsCount: 1, + commitsCount: 5, + filesCount: 3, + }, + { + release: 'v2.4.0', + timestamp: Date.now() - 86400000 * 14, // 2 weeks ago + newEventsCount: 2, + commitsCount: 20, + filesCount: 15, + }, +]; + +const mockFetchProjectReleases = { + data: { + project: { + releases: releasesMock, + }, + }, + errors: [], +}; + +export default mockFetchProjectReleases; diff --git a/src/api/projects/mocks/updateLastProjectVisit.mock.ts b/src/api/projects/mocks/updateLastProjectVisit.mock.ts new file mode 100644 index 000000000..2d068543a --- /dev/null +++ b/src/api/projects/mocks/updateLastProjectVisit.mock.ts @@ -0,0 +1,8 @@ +/** + * Mock for updateLastProjectVisit API function + * Returns success status for updating last project visit timestamp + */ +export default async function updateLastProjectVisitMock(projectId: string): Promise { + // Always return true in demo mode - no actual tracking needed + return true; +} diff --git a/src/api/user/index.js b/src/api/user/index.js index 6b4f7eecd..d65bf072a 100644 --- a/src/api/user/index.js +++ b/src/api/user/index.js @@ -12,6 +12,7 @@ import { } from './queries'; import * as api from '../index.ts'; import { validateUtmParams } from '../../components/utils/utm/utm.ts'; +import { withMockDemo } from '../../utils/withMockDemo.ts'; /** * @typedef {object} TokensPair @@ -83,9 +84,11 @@ export async function refreshTokens(refreshToken) { * * @returns {Promise>} */ -export async function fetchCurrentUser() { +export const fetchCurrentUser = withMockDemo( + () => import('./mocks/fetchCurrentUser.mock').then(m => m.default) +)(async function fetchCurrentUser() { return await api.call(QUERY_CURRENT_USER, {}, undefined, { allowErrors: true }); -} +}); /** * Update user profile diff --git a/src/api/user/mocks/fetchCurrentUser.mock.ts b/src/api/user/mocks/fetchCurrentUser.mock.ts new file mode 100644 index 000000000..e97eb348f --- /dev/null +++ b/src/api/user/mocks/fetchCurrentUser.mock.ts @@ -0,0 +1,16 @@ +/** + * Mock: fetchCurrentUser + * + * Returns the demo user account + */ + +import { DEMO_USER } from '@/api/mock-db'; + +export default function mockFetchCurrentUser() { + return { + data: { + me: DEMO_USER, + }, + errors: [], + }; +} diff --git a/src/api/workspaces/getAllWorkspacesWithProjects.mock.ts b/src/api/workspaces/getAllWorkspacesWithProjects.mock.ts deleted file mode 100644 index 2067c61f4..000000000 --- a/src/api/workspaces/getAllWorkspacesWithProjects.mock.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { Workspace } from '@/types/workspaces'; -import { DEMO_PROJECT_ID, DEMO_WORKSPACE_ID } from '@/utils/withDemoMock'; -import { DEMO_PROJECT } from './getWorkspaces.mock'; -import mockDailyEventsPortion from '@/api/events/fetchDailyEventsPortion.mock'; - -/** - * Demo workspace with demo project - */ -export const DEMO_WORKSPACE: Workspace = { - id: DEMO_WORKSPACE_ID, - name: 'Demo Workspace', - description: 'Here you can check how Hawk works', - image: 'https://static.hawk.so/fb59c4d7-db38-46d9-936e-c0d468ab1ea2.png', - inviteHash: 'demo-invite-hash-abc123def456', - team: [ - // { - // id: 'member-001-admin', - // user: { - // id: 'dev-user-000', - // email: 'dev@example.com', - // name: 'John Dev', - // image: 'https://ui-avatars.com/api/?name=John+Developer&background=4ECDC4', - // }, - // isAdmin: true, - // }, - // { - // id: 'member-002-dev', - // user: { - // id: 'dev-user-001', - // email: 'dev@example.com', - // name: 'John Developer', - // image: 'https://ui-avatars.com/api/?name=John+Developer&background=4ECDC4', - // }, - // isAdmin: false, - // }, - // { - // id: 'member-003-lead', - // user: { - // id: 'lead-user-001', - // email: 'team-lead@example.com', - // name: 'Sarah Team Lead', - // image: 'https://ui-avatars.com/api/?name=Sarah+Lead&background=95E1D3', - // }, - // isAdmin: true, - // }, - ], - plan: { - id: 'pro-plan-001', - name: '', - monthlyCharge: 49, - monthlyChargeCurrency: 'USD', - eventsLimit: 1000000, - }, - lastChargeDate: new Date('2026-01-15'), - isDebug: true, - isBlocked: false, - -}; - -/** - * Enrich project with daily events portion for display in workspace - * Create fresh copy to prevent mutation - */ -const projectWithEvents = { - ...DEMO_PROJECT, - dailyEventsPortion: mockDailyEventsPortion(), -}; - -const mockAllWorkspacesWithProjects = { - data: { - workspaces: [ - { - ...DEMO_WORKSPACE, - projects: [projectWithEvents], - }, - ], - }, - errors: [], -}; - -export default mockAllWorkspacesWithProjects; diff --git a/src/api/workspaces/getWorkspaces.mock.ts b/src/api/workspaces/getWorkspaces.mock.ts deleted file mode 100644 index 4c3a11e05..000000000 --- a/src/api/workspaces/getWorkspaces.mock.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { Project } from '@/types/project'; -import { DEMO_PROJECT_ID, DEMO_WORKSPACE_ID } from '@/utils/withDemoMock'; -import mockDailyEventsPortion from '@/api/events/fetchDailyEventsPortion.mock'; - -/** - * Demo project for workspace with comprehensive mock data - */ -export const DEMO_PROJECT: Project = { - id: DEMO_PROJECT_ID, - workspaceId: DEMO_WORKSPACE_ID, - token: 'token_6215743cf3ff6b80215cb183_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJpc3MiOiJodHRwczovL2hhd2suc28iLCJzdWIiOiIyMjQ2MjExODUyMjcwMzY4MzIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE2MDMzOTAwMDB9', - name: 'Production App', - uidAdded: { - id: 'dev-user-000', - email: 'dev@example.com' - }, - unreadCount: 5, - description: 'Production environment - real-time error tracking and monitoring for main application', - image: 'https://ui-avatars.com/api/?name=Production+App&background=FF6B6B', - notifications: [ - ], - eventGroupingPatterns: [ - { - id: 'pattern-001', - pattern: 'TypeError.*Cannot read.*undefined', - }, - { - id: 'pattern-002', - pattern: 'ReferenceError.*is not defined', - }, - { - id: 'pattern-003', - pattern: 'SyntaxError.*Unexpected token', - }, - ], - rateLimitSettings: { - N: 100000, - T: 86400, - }, -}; - -/** - * Enrich project with daily events portion for display in workspace - */ -const projectWithEvents = { - ...DEMO_PROJECT, - dailyEventsPortion: mockDailyEventsPortion, -}; - -/** - * Demo workspace with demo project - */ -const mockWorkspaceWithProject = { - data: { - workspaces: [ - { - id: DEMO_WORKSPACE_ID, - projects: [projectWithEvents], - }, - ], - }, - errors: [], -}; - -export default mockWorkspaceWithProject; diff --git a/src/api/workspaces/index.ts b/src/api/workspaces/index.ts index d54ec481a..d02acd70f 100644 --- a/src/api/workspaces/index.ts +++ b/src/api/workspaces/index.ts @@ -24,7 +24,7 @@ import type { WorkspaceSsoConfigInput } from '@/types/workspaces'; import type { APIResponse, APIResponseData } from '@/types/api'; -import { withDemoMock, DEMO_WORKSPACE_ID } from '@/utils/withDemoMock'; +import { withMockDemo } from '@/utils/withMockDemo'; interface CreateWorkspaceInput { /** @@ -61,8 +61,10 @@ export async function leaveWorkspace(workspaceId: string): Promise { * Returns all user's workspaces and project. * @returns */ -export async function getAllWorkspacesWithProjects(): Promise> { - const response = await api.call(QUERY_ALL_WORKSPACES_WITH_PROJECTS, undefined, undefined, { +export const getAllWorkspacesWithProjects = withMockDemo( + () => import('./mocks/getAllWorkspacesWithProjects.mock').then((m) => m.default) +)(async function getAllWorkspacesWithProjects(): Promise> { + return api.call(QUERY_ALL_WORKSPACES_WITH_PROJECTS, undefined, undefined, { initial: true, /** @@ -71,20 +73,7 @@ export async function getAllWorkspacesWithProjects(): Promise { - return ws.id !== DEMO_WORKSPACE_ID; - }); - - // add demo at the start of the list if there is no such workspace in the response - response.data.workspaces = [ - (await import('./getAllWorkspacesWithProjects.mock').then((m) => m.default)).data.workspaces[0], - ...response.data.workspaces, - ]; - - return response; -} +}); /** * Invites user to workspace by email diff --git a/src/api/workspaces/mocks/getAllWorkspacesWithProjects.mock.ts b/src/api/workspaces/mocks/getAllWorkspacesWithProjects.mock.ts new file mode 100644 index 000000000..57abec8b4 --- /dev/null +++ b/src/api/workspaces/mocks/getAllWorkspacesWithProjects.mock.ts @@ -0,0 +1,47 @@ +/** + * Mock: getAllWorkspacesWithProjects + * + * Returns demo workspace with project and events + */ + +import { DEMO_WORKSPACE, DEMO_PROJECT } from '@/api/mock-db'; +import type { DailyEventsPortion } from '@/types/events'; +import { DEMO_EVENTS } from '@/api/mock-db'; + +/** + * Create fresh daily events portion + */ +function createDailyEventsPortion(): DailyEventsPortion { + const now = Date.now(); + const dayTimestamp = Math.floor(now / 86400000) * 86400000; + + return { + nextCursor: null, + dailyEvents: DEMO_EVENTS.map((event, index) => ({ + id: `daily-${event.id}`, + groupingTimestamp: dayTimestamp, + count: event.totalCount, + affectedUsers: event.usersAffected, + event, + })), + }; +} + +export default function mockGetAllWorkspacesWithProjects() { + return { + data: { + workspaces: [ + { + ...DEMO_WORKSPACE, + projects: [ + { + ...DEMO_PROJECT, + dailyEventsPortion: createDailyEventsPortion(), + }, + ], + }, + ], + }, + errors: [], + }; +} diff --git a/src/components/auth/Login.vue b/src/components/auth/Login.vue index 926e6c32f..ead020893 100644 --- a/src/components/auth/Login.vue +++ b/src/components/auth/Login.vue @@ -21,6 +21,14 @@ {{ $t('authPages.continueWithSso') }} +
+ + {{ $t('authPages.showDemo') }} + +
@@ -28,7 +36,7 @@ + + diff --git a/src/components/AppShell.vue b/src/components/AppShell.vue index d75d3d5ad..71f810550 100644 --- a/src/components/AppShell.vue +++ b/src/components/AppShell.vue @@ -1,17 +1,9 @@