diff --git a/package-lock.json b/package-lock.json index f0227bae21..f3e2c1a152 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "workspaces": [ "packages/insomnia-testing", "packages/insomnia", + "packages/insomnia-analytics", "packages/insomnia-api", "packages/insomnia-inso", "packages/insomnia-smoke-test", @@ -18409,6 +18410,10 @@ "resolved": "packages/insomnia", "link": true }, + "node_modules/insomnia-analytics": { + "resolved": "packages/insomnia-analytics", + "link": true + }, "node_modules/insomnia-api": { "resolved": "packages/insomnia-api", "link": true @@ -29183,7 +29188,6 @@ "@rjsf/utils": "6.0.0-beta.15", "@rjsf/validator-ajv8": "6.0.0-beta.15", "@seald-io/nedb": "^4.1.1", - "@segment/analytics-node": "2.2.1", "@sentry/electron": "^6.5.0", "@stoplight/spectral-core": "^1.22.0", "@stoplight/spectral-formats": "^1.8.2", @@ -29336,6 +29340,13 @@ "@kong/insomnia-plugin-external-vault": "0.1.4-dev.20251224090833" } }, + "packages/insomnia-analytics": { + "version": "12.5.1-alpha.0", + "license": "Apache-2.0", + "dependencies": { + "@segment/analytics-node": "2.2.1" + } + }, "packages/insomnia-api": { "version": "12.5.1-alpha.0", "license": "Apache-2.0", @@ -29348,7 +29359,6 @@ "license": "Apache-2.0", "dependencies": { "@seald-io/nedb": "^4.1.1", - "@segment/analytics-node": "^2.2.1", "@stoplight/spectral-core": "^1.22.0", "@stoplight/spectral-formats": "^1.8.2", "@stoplight/spectral-ruleset-bundler": "1.7.0", diff --git a/package.json b/package.json index 06d77047f0..24861f27ee 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "workspaces": [ "packages/insomnia-testing", "packages/insomnia", + "packages/insomnia-analytics", "packages/insomnia-api", "packages/insomnia-inso", "packages/insomnia-smoke-test", diff --git a/packages/insomnia-analytics/package.json b/packages/insomnia-analytics/package.json new file mode 100644 index 0000000000..a4c328f7be --- /dev/null +++ b/packages/insomnia-analytics/package.json @@ -0,0 +1,36 @@ +{ + "private": true, + "name": "insomnia-analytics", + "license": "Apache-2.0", + "version": "12.5.1-alpha.0", + "author": "Kong ", + "description": "Shared analytics client for the Insomnia desktop app and CLI", + "repository": { + "type": "git", + "url": "git+https://github.com/Kong/insomnia.git", + "directory": "packages/insomnia-analytics" + }, + "bugs": { + "url": "https://github.com/kong/insomnia/issues" + }, + "homepage": "https://github.com/Kong/insomnia#readme", + "sideEffects": false, + "exports": { + ".": { + "import": "./src/index.ts", + "types": "./src/index.ts" + }, + "./events": { + "import": "./src/events.ts", + "types": "./src/events.ts" + } + }, + "scripts": { + "lint": "eslint . --ext .ts --cache", + "type-check": "tsc --noEmit --project tsconfig.json", + "test": "vitest run" + }, + "dependencies": { + "@segment/analytics-node": "2.2.1" + } +} diff --git a/packages/insomnia-analytics/src/analytics.test.ts b/packages/insomnia-analytics/src/analytics.test.ts new file mode 100644 index 0000000000..0bf2e2dd3f --- /dev/null +++ b/packages/insomnia-analytics/src/analytics.test.ts @@ -0,0 +1,73 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mockTrack = vi.fn(); +const mockPage = vi.fn(); +const mockCloseAndFlush = vi.fn().mockResolvedValue(void 0); + +vi.mock('@segment/analytics-node', () => ({ + Analytics: vi.fn(() => ({ + track: mockTrack, + page: mockPage, + closeAndFlush: mockCloseAndFlush, + })), +})); + +describe('InsomniaAnalytics', () => { + beforeEach(() => { + vi.resetModules(); + mockTrack.mockReset(); + mockPage.mockReset(); + mockCloseAndFlush.mockReset().mockResolvedValue(void 0); + }); + + it('forwards track calls with platform-tagged properties and an app+os context', async () => { + const { InsomniaAnalytics } = await import('./analytics'); + const analytics = new InsomniaAnalytics({ + writeKey: 'test', + app: { appName: 'inso', appVersion: '1.2.3', osVersion: '14.0', platform: 'cli' }, + }); + + analytics.track({ event: 'inso_run_test', properties: { suite: 'echo' }, anonymousId: 'anon-1' }); + + expect(mockTrack).toHaveBeenCalledTimes(1); + const [payload] = mockTrack.mock.calls[0]; + expect(payload).toMatchObject({ + event: 'inso_run_test', + anonymousId: 'anon-1', + userId: '', + properties: { suite: 'echo', platform: 'cli' }, + context: { app: { name: 'inso', version: '1.2.3' }, os: { version: '14.0' } }, + }); + }); + + it('resolves osVersion lazily when provided as a function', async () => { + const { InsomniaAnalytics } = await import('./analytics'); + let v = '1.0'; + const analytics = new InsomniaAnalytics({ + writeKey: 'test', + app: { appName: 'app', appVersion: '0.1', osVersion: () => v, platform: 'app' }, + }); + + v = '2.0'; + analytics.track({ event: 'App Started', anonymousId: 'a', userId: 'u' }); + + const [payload] = mockTrack.mock.calls[0]; + expect(payload.context.os.version).toBe('2.0'); + }); + + it('routes analytics errors through the onError handler', async () => { + const onError = vi.fn(); + const { InsomniaAnalytics } = await import('./analytics'); + const analytics = new InsomniaAnalytics({ + writeKey: 'test', + app: { appName: 'app', appVersion: '0.1', osVersion: '1.0', platform: 'app' }, + onError, + }); + + analytics.track({ event: 'App Started', anonymousId: 'a' }); + const [, callback] = mockTrack.mock.calls[0]; + callback(new Error('segment failure')); + + expect(onError).toHaveBeenCalledWith(expect.any(Error)); + }); +}); diff --git a/packages/insomnia-analytics/src/analytics.ts b/packages/insomnia-analytics/src/analytics.ts new file mode 100644 index 0000000000..774ce1adee --- /dev/null +++ b/packages/insomnia-analytics/src/analytics.ts @@ -0,0 +1,103 @@ +import { Analytics, type AnalyticsSettings } from '@segment/analytics-node'; + +export interface AppContext { + appName: string; + appVersion: string; + osVersion: string | (() => string); + platform: 'app' | 'cli'; +} + +export interface TrackOptions { + event: string; + properties?: Record; + anonymousId: string; + userId?: string; +} + +export interface PageOptions { + name: string; + anonymousId: string; + userId?: string; +} + +export interface InsomniaAnalyticsOptions { + writeKey: string; + app: AppContext; + settings?: Omit; + onError?: (error: unknown) => void; +} + +export function getNormalizedOsName(platform: NodeJS.Platform | string): string { + switch (platform) { + case 'darwin': { + return 'mac'; + } + case 'win32': { + return 'windows'; + } + default: { + return platform; + } + } +} + +export class InsomniaAnalytics { + private readonly client: Analytics; + private readonly app: AppContext; + private readonly onError: (error: unknown) => void; + + constructor({ writeKey, app, settings, onError }: InsomniaAnalyticsOptions) { + this.client = new Analytics({ writeKey, ...settings }); + this.app = app; + this.onError = onError ?? (() => {}); + } + + track({ event, properties, anonymousId, userId }: TrackOptions): void { + this.client.track( + { + event, + anonymousId, + userId: userId ?? '', + properties: { ...properties, platform: this.app.platform }, + context: this.buildContext(), + }, + error => { + if (error) { + this.onError(error); + } + }, + ); + } + + page({ name, anonymousId, userId }: PageOptions): void { + this.client.page( + { + name, + anonymousId, + userId: userId ?? '', + context: this.buildContext(), + }, + error => { + if (error) { + this.onError(error); + } + }, + ); + } + + async closeAndFlush(timeoutMs = 5000): Promise { + try { + await this.client.closeAndFlush({ timeout: timeoutMs }); + } catch (error) { + this.onError(error); + } + } + + private buildContext() { + const osVersion = typeof this.app.osVersion === 'function' ? this.app.osVersion() : this.app.osVersion; + return { + app: { name: this.app.appName, version: this.app.appVersion }, + os: { name: getNormalizedOsName(process.platform), version: osVersion }, + }; + } +} diff --git a/packages/insomnia-analytics/src/events.test.ts b/packages/insomnia-analytics/src/events.test.ts new file mode 100644 index 0000000000..2072bdf6e1 --- /dev/null +++ b/packages/insomnia-analytics/src/events.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; + +import { AnalyticsEvent, InsoEvent } from './events'; + +describe('events', () => { + it('AnalyticsEvent has expected entries', () => { + expect(AnalyticsEvent.appStarted).toBe('App Started'); + expect(AnalyticsEvent.unitTestRun).toBe('Ran Individual Unit Test'); + expect(AnalyticsEvent.installPlugin).toBe('Plugin Installed'); + }); + + it('InsoEvent prefixes with inso_', () => { + expect(InsoEvent.runTest).toBe('inso_run_test'); + expect(InsoEvent.script).toBe('inso_script'); + }); +}); diff --git a/packages/insomnia-analytics/src/events.ts b/packages/insomnia-analytics/src/events.ts new file mode 100644 index 0000000000..50dd647742 --- /dev/null +++ b/packages/insomnia-analytics/src/events.ts @@ -0,0 +1,142 @@ +export enum AnalyticsEvent { + appStarted = 'App Started', + analyticsDisabled = 'Analytics Disabled', + collectionCreate = 'Collection Created', + dataExport = 'Data Exported', + exportCompleted = 'Export Completed', + dataImport = 'Data Imported', + importStarted = 'Import Started', + importScanned = 'Import Scanned', + importCompleted = 'Import Completed', + importLoginRequired = 'Import Login Required', + importResumedAfterLogin = 'Import Resumed After Login', + importedRequestFirstSend = 'Imported Request First Send', + documentCreate = 'Document Created', + mockCreateModalOpened = 'Mock Server Create Modal Opened', + mockCreate = 'Mock Created', + mockEdit = 'Mock Server Edited', + mockDelete = 'Mock Server Deleted', + mockRouteCreate = 'Mock Route Created', + mockRouteEdit = 'Mock Route Edited', + mockRouteDelete = 'Mock Route Deleted', + generateCollection = 'Generated Collection', + generateCollectionFromMock = 'Generate Collection From Mock', + environmentCreate = 'Environment Created', + loginSuccess = 'Login Success', + inviteTrigger = 'Invite Triggered From App', + exportAllCollections = 'Exported All Collections', + kongConnected = 'Kong Connected', + kongSync = 'Kong Synced', + requestBodyTypeSelect = 'Request Body Type Selected', + requestCreated = 'Request Created', + requestExecuted = 'Request Executed', + requestEdit = 'Request Edited', + requestDeleted = 'Request Deleted', + requestRenamed = 'Request Renamed', + requestUrlCopied = 'Request URL Copied', + collectionRunExecute = 'Collection Run Executed', + projectLocalCreate = 'Local Project Created', + projectLocalDelete = 'Local Project Deleted', + selectScratchpad = 'Scratchpad Selected at Login', + syncConflictResolutionStart = 'Sync Conflict Resolution Started', + syncConflictResolutionCompleteMine = 'Sync Conflict Resolution Completed Mine', + syncConflictResolutionCompleteTheirs = 'Sync Conflict Resolution Completed Theirs', + testSuiteCreate = 'Test Suite Created', + testSuiteDelete = 'Test Suite Deleted', + unitTestCreate = 'Unit Test Created', + unitTestDelete = 'Unit Test Deleted', + unitTestRun = 'Ran Individual Unit Test', + unitTestRunAll = 'Ran All Unit Tests', + vcsSyncStart = 'VCS Sync Started', + vcsSyncComplete = 'VCS Sync Completed', + vcsAction = 'VCS Action Executed', + gitAuthenticationCompleted = 'Git Authentication Completed', + gitAuthenticationUpdated = 'Git Authentication Updated', + buttonClick = 'Button Clicked', + inviteMember = 'Invite Sent', + inviteResent = 'Invite Resent', + inviteRevoked = 'Invite Revoked', + projectCreated = 'Project Created', + projectUpdated = 'Project Updated', + exportStarted = 'Export Started', + exportRequestsChosen = 'Export Requests Chosen', + recommendCommitsGenerated = 'Recommend Commits Generated', + recommendCommitsSaved = 'Recommend Commits Saved', + recommendCommitsCancelled = 'Recommend Commits Cancelled', + recommendCommitsClicked = 'Recommend Commits Clicked', + mcpClientWorkspaceCreate = 'MCP Client Workspace Created', + mcpClientAdded = 'MCP Client Added', + mcpClientConnected = 'MCP Client Connected', + mcpClientDisconnected = 'MCP Client Disconnected', + mcpToolCalled = 'MCP Tool Called', + mcpResourceRead = 'MCP Resource Read', + mcpPromptCalled = 'MCP Prompt Called', + inviteNotPermitted = 'Invite Not Permitted', + responseToMockClicked = 'Response To Mock Clicked', + gitSyncButtonClicked = 'Git Sync Button Clicked', + preferencesViewed = 'Preferences Viewed', + copyAsCurl = 'Copied As cURL', + themeChanged = 'Theme Changed', + generateCodeClicked = 'Generate Code Clicked', + generateCodeLanguageChanged = 'Generate Code Language Changed', + filterCreatedHomePage = 'Filter Created From Home Page', + filterCreatedProjects = 'Filter Created Projects', + filterCreatedRequests = 'Filter Created Requests', + filterCreatedResponseBody = 'Filter Created Response Body', + aiFeatureEnabled = 'AI Feature Enabled', + aiFeatureDisabled = 'AI Feature Disabled', + installPlugin = 'Plugin Installed', + + homepageFiltered = 'homepage-filtered', + quickSearchOpenedByKeyboard = 'quick-search-opened-by-keyboard', + quickSearchOpenedByMouse = 'quick-search-opened-by-mouse', + statusbarLeftbarToggled = 'statusbar-leftbar-toggled', + statusbarTopbarToggled = 'statusbar-topbar-toggled', + statusbarOrphanedProjectsClicked = 'statusbar-orphaned-projects-clicked', + designerGenerateMockClicked = 'designer-generate-mock-clicked', + designerPreviewToggled = 'designer-preview-toggled', + requestEnvironmentClicked = 'request-environment-clicked', + requestAddCookiesClicked = 'request-add-cookies-clicked', + requestAddCertificatesClicked = 'request-add-certificates-clicked', + requestListSortClicked = 'request-list-sort-clicked', + requestListExpandCollapseClicked = 'request-list-expand-collapse-clicked', + requestParamsDescriptionToggled = 'request-params-description-toggled', + requestParamsImportFromURLClicked = 'request-params-import-from-URL-clicked', + requestParamsBulkEditToggled = 'request-params-bulk-edit-toggled', + responsePreviewJSONPathEntered = 'response-preview-jsonpath-entered', + requestBodyBeautifyClicked = 'request-body-beautify-clicked', + requestHeadersDescriptionToggled = 'request-headers-description-toggled', + requestHeadersBulkEditToggled = 'request-headers-bulk-edit-toggled', + requestScriptsPreScriptSnippetAdded = 'request-scripts-prescript-snippet-added', + requestScriptsPostScriptSnippetAdded = 'request-scripts-postscript-snippet-added', + responseHeadersCopyAllClicked = 'response-headers-copy-all-clicked', + responseCookiesManageCookiesClicked = 'response-cookies-manage-cookies-clicked', + requestOpenInNewTabClicked = 'request-open-in-new-tab-clicked', + requestListMenuPinClicked = 'request-list-menu-pin-clicked', + requestListMenuDuplicateClicked = 'request-list-menu-duplicate-clicked', + requestListMenuRenameClicked = 'request-list-menu-rename-clicked', + requestListMenuSettingsClicked = 'request-list-menu-settings-clicked', + requestSendMenuGenerateCodeClicked = 'request-send-menu-generate-code-clicked', + requestSendMenuSendAfterDelayClicked = 'request-send-menu-send-after-delay-clicked', + requestSendMenuRepeatAfterIntervalClicked = 'request-send-menu-repeat-after-interval-clicked', + requestSendMenuDownloadAfterSendClicked = 'request-send-menu-download-after-send-clicked', + requestSendMenuSendAndDownloadClicked = 'request-send-menu-send-and-download-clicked', + mcpListExpandCollapseClicked = 'mcp-list-expand-collapse-clicked', + mcpListFiltered = 'mcp-list-filtered', + mcpRequestParamsBeautifyClicked = 'mcp-request-params-beautify-clicked', + mcpRequestHeadersDescriptionToggled = 'mcp-request-headers-description-toggled', + mcpRequestRootsNotifyClicked = 'mcp-request-roots-notify-clicked', + mcpResponseHeadersCopyAllClicked = 'mcp-response-headers-copy-all-clicked', + kongKonnectPatValidated = 'kong-konnect-pat-validated', + kongKonnectSyncCompleted = 'kong-konnect-sync-completed', + emptyStateSendRequestClicked = 'empty-state-send-request-clicked', + emptyStateCreateDocumentClicked = 'empty-state-create-document-clicked', +} + +export enum InsoEvent { + runTest = 'inso_run_test', + runCollection = 'inso_run_collection', + lintSpec = 'inso_lint_spec', + exportSpec = 'inso_export_spec', + script = 'inso_script', +} diff --git a/packages/insomnia-analytics/src/index.ts b/packages/insomnia-analytics/src/index.ts new file mode 100644 index 0000000000..c369a9583b --- /dev/null +++ b/packages/insomnia-analytics/src/index.ts @@ -0,0 +1,2 @@ +export * from './events'; +export * from './analytics'; diff --git a/packages/insomnia-analytics/tsconfig.json b/packages/insomnia-analytics/tsconfig.json new file mode 100644 index 0000000000..1cf9c3d3a3 --- /dev/null +++ b/packages/insomnia-analytics/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2020", + "allowJs": false, + "resolveJsonModule": true, + "moduleResolution": "bundler", + "isolatedModules": true, + "module": "ESNext", + "sourceMap": true, + "baseUrl": ".", + "rootDirs": ["."], + "lib": ["ES2023", "DOM"], + "types": [], + "strict": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "useUnknownInCatchVariables": false, + "verbatimModuleSyntax": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/packages/insomnia-analytics/vitest.config.ts b/packages/insomnia-analytics/vitest.config.ts new file mode 100644 index 0000000000..4ac6027d57 --- /dev/null +++ b/packages/insomnia-analytics/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + }, +}); diff --git a/packages/insomnia-inso/package.json b/packages/insomnia-inso/package.json index 7d75429a7e..ba587680b0 100644 --- a/packages/insomnia-inso/package.json +++ b/packages/insomnia-inso/package.json @@ -51,7 +51,6 @@ "shellwords": "^1.0.1" }, "dependencies": { - "@segment/analytics-node": "^2.2.1", "@seald-io/nedb": "^4.1.1", "@stoplight/spectral-core": "^1.22.0", "@stoplight/spectral-formats": "^1.8.2", diff --git a/packages/insomnia-inso/src/analytics.ts b/packages/insomnia-inso/src/analytics.ts index ba4c0bf657..2f116f99cd 100644 --- a/packages/insomnia-inso/src/analytics.ts +++ b/packages/insomnia-inso/src/analytics.ts @@ -1,7 +1,7 @@ import os from 'node:os'; -import { Analytics } from '@segment/analytics-node'; import { getSegmentWriteKey } from 'insomnia/src/common/constants'; +import { InsoEvent, InsomniaAnalytics } from 'insomnia-analytics'; import { v4 as uuidv4 } from 'uuid'; import type { Settings } from '~/insomnia-data'; @@ -10,15 +10,18 @@ import packageJson from '../package.json'; import neDbAdapter from './db/adapters/ne-db-adapter'; import { getAppDataDir, getDefaultProductName } from './util'; -export enum InsoEvent { - runTest = 'inso_run_test', - runCollection = 'inso_run_collection', - lintSpec = 'inso_lint_spec', - exportSpec = 'inso_export_spec', - script = 'inso_script', -} +export { InsoEvent }; + +const analytics = new InsomniaAnalytics({ + writeKey: getSegmentWriteKey(), + app: { + appName: 'inso', + appVersion: process.env.VERSION || packageJson.version, + osVersion: () => os.release(), + platform: 'cli', + }, +}); -const analyticsClient = new Analytics({ writeKey: getSegmentWriteKey() }); let deviceId: string | null = null; let localSettings: Settings | null = null; @@ -54,25 +57,16 @@ const getDeviceId = async (): Promise => { return deviceId; }; -const getOsName = (): string => { - switch (process.platform) { - case 'darwin': { - return 'mac'; - } - case 'win32': { - return 'windows'; - } - default: { - return process.platform; - } - } -}; - export const trackInsoEvent = async (event: InsoEvent, properties?: Record): Promise => { if (process.env.NODE_ENV === 'test') { return; } + // new for v13 - provide a way to disable analytics + if (process.env.INSO_TELEMETRY_DISABLED) { + return; + } + const settings = await getLocalSettings(); if (settings && !settings.enableAnalytics) { return; @@ -80,40 +74,10 @@ export const trackInsoEvent = async (event: InsoEvent, properties?: Record { - // Silently fail - }, - ); - } catch { - // Silently fail - } + analytics.track({ event, anonymousId, properties }); + } catch {} }; export const flushAnalytics = async (): Promise => { - try { - await analyticsClient.closeAndFlush({ timeout: 5000 }); - } catch { - // Silently fail - } + await analytics.closeAndFlush(5000); }; diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index 8b4f30ee99..9995d1830e 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -64,7 +64,6 @@ "@rjsf/utils": "6.0.0-beta.15", "@rjsf/validator-ajv8": "6.0.0-beta.15", "@seald-io/nedb": "^4.1.1", - "@segment/analytics-node": "2.2.1", "@sentry/electron": "^6.5.0", "@stoplight/spectral-core": "^1.22.0", "@stoplight/spectral-formats": "^1.8.2", diff --git a/packages/insomnia/src/__tests__/install-plugin.test.ts b/packages/insomnia/src/__tests__/install-plugin.test.ts index 91e8a62391..f90edea2d6 100644 --- a/packages/insomnia/src/__tests__/install-plugin.test.ts +++ b/packages/insomnia/src/__tests__/install-plugin.test.ts @@ -66,8 +66,8 @@ vi.mock('../main/install-plugin', async () => { }); vi.mock('../main/analytics', () => ({ - trackSegmentEvent: vi.fn(), - SegmentEvent: { + trackAnalyticsEvent: vi.fn(), + AnalyticsEvent: { installPlugin: 'Plugin Installed', }, })); diff --git a/packages/insomnia/src/entry.main.ts b/packages/insomnia/src/entry.main.ts index abcf0f8c70..abe693822c 100644 --- a/packages/insomnia/src/entry.main.ts +++ b/packages/insomnia/src/entry.main.ts @@ -21,7 +21,7 @@ import { registerLLMConfigServiceAPI } from '~/main/llm-config-service'; import { userDataFolder } from '../config/config.json'; import { getAppVersion, getProductName, isDevelopment } from './common/constants'; import { isMac } from './common/platform'; -import { SegmentEvent, trackSegmentEvent } from './main/analytics'; +import { AnalyticsEvent, trackAnalyticsEvent } from './main/analytics'; import { registerInsomniaProtocols } from './main/api.protocol'; import { backupIfNewerVersionAvailable } from './main/backup'; import { registerSyncHandlers } from './main/cloud-sync/ipc'; @@ -365,7 +365,7 @@ async function _trackStats() { const settings = await services.settings.get(); - trackSegmentEvent(SegmentEvent.appStarted, { + trackAnalyticsEvent(AnalyticsEvent.appStarted, { localProjects, remoteProjects, createdRequests: stats.createdRequests, diff --git a/packages/insomnia/src/entry.preload.ts b/packages/insomnia/src/entry.preload.ts index ed7b80188a..6ad8416d4e 100644 --- a/packages/insomnia/src/entry.preload.ts +++ b/packages/insomnia/src/entry.preload.ts @@ -289,7 +289,7 @@ const main: Window['main'] = { secretStorage, electronStorage, sync, - trackSegmentEvent: options => ipcRenderer.send('trackSegmentEvent', options), + trackAnalyticsEvent: options => ipcRenderer.send('trackAnalyticsEvent', options), trackPageView: options => ipcRenderer.send('trackPageView', options), setCurrentOrganizationId: organizationId => ipcRenderer.send('analytics.setOrganizationId', organizationId), showNunjucksContextMenu: options => ipcRenderer.send('show-nunjucks-context-menu', options), diff --git a/packages/insomnia/src/konnect/__tests__/sync.test.ts b/packages/insomnia/src/konnect/__tests__/sync.test.ts index 376f66a490..9bdbc2695d 100644 --- a/packages/insomnia/src/konnect/__tests__/sync.test.ts +++ b/packages/insomnia/src/konnect/__tests__/sync.test.ts @@ -1,7 +1,7 @@ /** * Tests run against the in-memory NeDB initialized by setup-vitest.ts. * fetch is mocked per-test to return shaped Konnect API responses. - * window.main is stubbed globally so trackSegmentEvent calls don't throw. + * window.main is stubbed globally so trackAnalyticsEvent calls don't throw. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -101,13 +101,13 @@ const konnectRequests = (docs: any[]) => docs.filter((r: any) => r.konnectRouteK const konnectWorkspaces = (docs: any[]) => docs.filter((w: any) => w.konnectServiceId != null); const konnectProjects = (docs: any[]) => docs.filter((p: any) => p.konnectControlPlaneId != null); -const trackSegmentEvent = vi.fn(); +const trackAnalyticsEvent = vi.fn(); beforeEach(async () => { // Re-init with fresh in-memory NeDB buckets — clean slate for every test. await initDatabase(mainDatabase, { inMemoryOnly: true }, true); resetV4Counter(); - vi.stubGlobal('window', { main: { trackSegmentEvent } }); + vi.stubGlobal('window', { main: { trackAnalyticsEvent } }); }); afterEach(() => { diff --git a/packages/insomnia/src/main/analytics.ts b/packages/insomnia/src/main/analytics.ts index b78061c767..59b47ec41e 100644 --- a/packages/insomnia/src/main/analytics.ts +++ b/packages/insomnia/src/main/analytics.ts @@ -1,8 +1,8 @@ import crypto from 'node:crypto'; -import { Analytics } from '@segment/analytics-node'; import * as Sentry from '@sentry/electron/main'; import { net } from 'electron'; +import { AnalyticsEvent, InsomniaAnalytics } from 'insomnia-analytics'; import { v4 as uuidv4 } from 'uuid'; import { services } from '~/insomnia-data'; @@ -15,7 +15,8 @@ import { getSegmentWriteKey, PLAYWRIGHT_TEST, } from '../common/constants'; -import { platform } from '../common/platform'; + +export { AnalyticsEvent }; let _currentOrganizationId: string | undefined; @@ -23,18 +24,29 @@ export function setCurrentOrganizationId(id: string | undefined): void { _currentOrganizationId = id; } -const analytics = new Analytics({ +const analytics = new InsomniaAnalytics({ writeKey: getSegmentWriteKey(), - httpClient: { - makeRequest(_options) { - return net.fetch(_options.url, { - method: _options.method, - headers: _options.headers, - body: _options.body, - signal: AbortSignal.timeout(_options.httpRequestTimeout), - }); + app: { + appName: getProductName(), + appVersion: getAppVersion(), + osVersion: () => process.getSystemVersion(), + platform: 'app', + }, + settings: { + httpClient: { + makeRequest(_options) { + return net.fetch(_options.url, { + method: _options.method, + headers: _options.headers, + body: _options.body, + signal: AbortSignal.timeout(_options.httpRequestTimeout), + }); + }, }, }, + onError: error => { + console.warn('[analytics] Error sending analytics event', error); + }, }); const getDeviceId = async () => { @@ -42,48 +54,11 @@ const getDeviceId = async () => { return settings.deviceId || (await services.settings.update(settings, { deviceId: uuidv4() })).deviceId; }; -export enum SegmentEvent { - appStarted = 'App Started', - collectionCreate = 'Collection Created', - dataExport = 'Data Exported', - dataImport = 'Data Imported', - loginSuccess = 'Login Success', - documentCreate = 'Document Created', - kongConnected = 'Kong Connected', - kongSync = 'Kong Synced', - requestBodyTypeSelect = 'Request Body Type Selected', - requestCreated = 'Request Created', - requestExecuted = 'Request Executed', - collectionRunExecute = 'Collection Run Executed', - projectLocalCreate = 'Local Project Created', - projectLocalDelete = 'Local Project Deleted', - testSuiteCreate = 'Test Suite Created', - testSuiteDelete = 'Test Suite Deleted', - unitTestCreate = 'Unit Test Created', - unitTestDelete = 'Unit Test Deleted', - unitTestRun = 'Ran Individual Unit Test', - unitTestRunAll = 'Ran All Unit Tests', - vcsSyncStart = 'VCS Sync Started', - vcsSyncComplete = 'VCS Sync Completed', - vcsAction = 'VCS Action Executed', - gitAuthenticationCompleted = 'Git Authentication Completed', - gitAuthenticationUpdated = 'Git Authentication Updated', - buttonClick = 'Button Clicked', - aiFeatureEnabled = 'AI Feature Enabled', - aiFeatureDisabled = 'AI Feature Disabled', - mcpClientConnected = 'MCP Client Connected', - mcpClientDisconnected = 'MCP Client Disconnected', - mcpToolCalled = 'MCP Tool Called', - mcpResourceRead = 'MCP Resource Read', - mcpPromptCalled = 'MCP Prompt Called', - installPlugin = 'Plugin Installed', -} - function hashString(input: string) { return crypto.createHash('sha256').update(input).digest('hex'); } -export async function trackSegmentEvent(event: SegmentEvent, properties?: Record) { +export async function trackAnalyticsEvent(event: AnalyticsEvent, properties?: Record) { if (PLAYWRIGHT_TEST) { return; } @@ -93,46 +68,34 @@ export async function trackSegmentEvent(event: SegmentEvent, properties?: Record userSession.hashedAccountId = userSession?.accountId ? hashString(userSession.accountId) : ''; } const allowAnalytics = settings.enableAnalytics || userSession?.hashedAccountId; - if (allowAnalytics) { - try { - const anonymousId = (await getDeviceId()) ?? ''; - const context = { - app: { name: getProductName(), version: getAppVersion() }, - os: { name: _getOsName(), version: process.getSystemVersion() }, - }; + if (!allowAnalytics) { + return; + } - analytics.track( - { - event, - properties: { - ...(_currentOrganizationId && { organization_id: _currentOrganizationId }), - ...properties, - platform: 'app', - }, - context, - anonymousId, - userId: userSession?.hashedAccountId || '', + try { + const anonymousId = (await getDeviceId()) ?? ''; + analytics.track({ + event, + properties: { + ...(_currentOrganizationId && { organization_id: _currentOrganizationId }), + ...properties, + }, + anonymousId, + userId: userSession?.hashedAccountId || '', + }); + } catch (error: unknown) { + console.warn('[analytics] Unexpected error while sending analytics event', error); + } finally { + if (!userSession?.hashedAccountId && [AnalyticsEvent.unitTestRun, AnalyticsEvent.unitTestRunAll].includes(event)) { + Sentry.captureException(`Run tests by anonymous`, { + tags: { + source: 'main/analytics', }, - error => { - if (error) { - console.warn('[analytics] Error sending segment event', error); - } + extra: { + organizationId: properties?.organizationId || '', + projectId: properties?.projectId || '', }, - ); - } catch (error: unknown) { - console.warn('[analytics] Unexpected error while sending segment event', error); - } finally { - if (!userSession?.hashedAccountId && [SegmentEvent.unitTestRun, SegmentEvent.unitTestRunAll].includes(event)) { - Sentry.captureException(`Run tests by anonymous`, { - tags: { - source: 'main/analytics', - }, - extra: { - organizationId: properties?.organizationId || '', - projectId: properties?.projectId || '', - }, - }); - } + }); } } } @@ -148,48 +111,24 @@ export async function trackPageView(name: string) { } const allowAnalytics = settings.enableAnalytics || userSession?.hashedAccountId; - if (allowAnalytics) { - try { - const anonymousId = (await getDeviceId()) ?? ''; - const context = { - app: { name: getProductName(), version: getAppVersion() }, - os: { name: _getOsName(), version: process.getSystemVersion() }, - }; + if (!allowAnalytics) { + return; + } - analytics.page({ name, context, anonymousId, userId: userSession?.hashedAccountId }, error => { - if (error) { - console.warn('[analytics] Error sending segment event', error); - } + try { + const anonymousId = (await getDeviceId()) ?? ''; + analytics.page({ name, anonymousId, userId: userSession?.hashedAccountId }); + + if (userSession?.id) { + net.fetch(getApiBaseURL() + '/v1/telemetry/', { + method: 'POST', + headers: new Headers({ + 'X-Session-Id': userSession?.id, + 'X-Insomnia-Client': getClientString(), + }), }); - - if (userSession?.id) { - net.fetch(getApiBaseURL() + '/v1/telemetry/', { - method: 'POST', - headers: new Headers({ - 'X-Session-Id': userSession?.id, - 'X-Insomnia-Client': getClientString(), - }), - }); - } - } catch (error: unknown) { - console.warn('[analytics] Unexpected error while sending segment event', error); - } - } -} - -// ~~~~~~~~~~~~~~~~~ // -// Private Functions // -// ~~~~~~~~~~~~~~~~~ // -function _getOsName() { - switch (platform) { - case 'darwin': { - return 'mac'; - } - case 'win32': { - return 'windows'; - } - default: { - return platform; } + } catch (error: unknown) { + console.warn('[analytics] Unexpected error while sending analytics event', error); } } diff --git a/packages/insomnia/src/main/git-service.ts b/packages/insomnia/src/main/git-service.ts index 19499f86c1..6b85b60012 100644 --- a/packages/insomnia/src/main/git-service.ts +++ b/packages/insomnia/src/main/git-service.ts @@ -66,7 +66,7 @@ import { routableFSClient } from '../sync/git/routable-fs-client'; import { shallowClone } from '../sync/git/shallow-clone'; import type { AutoResolvedConflict, MergeConflict } from '../sync/types'; import { invariant } from '../utils/invariant'; -import { SegmentEvent, trackSegmentEvent } from './analytics'; +import { AnalyticsEvent, trackAnalyticsEvent } from './analytics'; import { ipcMainHandle } from './ipc/electron'; // Initialize Git Remote Providers on module load @@ -153,7 +153,7 @@ export function getErrorMessage(error: unknown): string { // Non-Error objects return 'Unknown Error'; } -export function vcsSegmentEventProperties(type: 'git', action: VCSAction, error?: string) { +export function vcsEventProperties(type: 'git', action: VCSAction, error?: string) { return { type, action, error }; } @@ -1097,8 +1097,8 @@ export const cloneGitRepoAction = async ({ } if (!projectId) { - trackSegmentEvent(SegmentEvent.vcsSyncStart, { - ...vcsSegmentEventProperties('git', 'clone'), + trackAnalyticsEvent(AnalyticsEvent.vcsSyncStart, { + ...vcsEventProperties('git', 'clone'), provider, repoId: repoSettingsPatch._id, }); @@ -1230,8 +1230,8 @@ export const cloneGitRepoAction = async ({ }); await database.flushChanges(bufferId); - trackSegmentEvent(SegmentEvent.vcsSyncComplete, { - ...vcsSegmentEventProperties('git', 'clone'), + trackAnalyticsEvent(AnalyticsEvent.vcsSyncComplete, { + ...vcsEventProperties('git', 'clone'), providerName, repoId: repoSettingsPatch._id, }); @@ -1245,8 +1245,8 @@ export const cloneGitRepoAction = async ({ const project = await services.project.getById(projectId); invariant(project, 'Project not found'); - trackSegmentEvent(SegmentEvent.vcsSyncStart, { - ...vcsSegmentEventProperties('git', 'clone'), + trackAnalyticsEvent(AnalyticsEvent.vcsSyncStart, { + ...vcsEventProperties('git', 'clone'), provider, repoId: repoSettingsPatch._id, }); @@ -1304,8 +1304,8 @@ export const cloneGitRepoAction = async ({ }); await services.apiSpec.getOrCreateForParentId(workspace._id); - trackSegmentEvent(SegmentEvent.vcsSyncComplete, { - ...vcsSegmentEventProperties('git', 'clone', 'no directory found'), + trackAnalyticsEvent(AnalyticsEvent.vcsSyncComplete, { + ...vcsEventProperties('git', 'clone', 'no directory found'), providerName: provider, repoId: repoSettingsPatch._id, }); @@ -1323,8 +1323,8 @@ export const cloneGitRepoAction = async ({ const workspaces = await inMemoryFsClient.promises.readdir(workspaceBase); if (workspaces.length === 0) { - trackSegmentEvent(SegmentEvent.vcsSyncComplete, { - ...vcsSegmentEventProperties('git', 'clone', 'no workspaces found'), + trackAnalyticsEvent(AnalyticsEvent.vcsSyncComplete, { + ...vcsEventProperties('git', 'clone', 'no workspaces found'), providerName: provider, repoId: repoSettingsPatch._id, }); @@ -1335,8 +1335,8 @@ export const cloneGitRepoAction = async ({ } if (workspaces.length > 1) { - trackSegmentEvent(SegmentEvent.vcsSyncComplete, { - ...vcsSegmentEventProperties('git', 'clone', 'multiple workspaces found'), + trackAnalyticsEvent(AnalyticsEvent.vcsSyncComplete, { + ...vcsEventProperties('git', 'clone', 'multiple workspaces found'), providerName: provider, repoId: repoSettingsPatch._id, }); @@ -1424,8 +1424,8 @@ export const cloneGitRepoAction = async ({ // Flush DB changes await database.flushChanges(bufferId); - trackSegmentEvent(SegmentEvent.vcsSyncComplete, { - ...vcsSegmentEventProperties('git', 'clone'), + trackAnalyticsEvent(AnalyticsEvent.vcsSyncComplete, { + ...vcsEventProperties('git', 'clone'), providerName: provider, repoId: repoSettingsPatch._id, }); @@ -1601,8 +1601,8 @@ export const commitToGitRepoAction = async ({ providerName = credentials.provider; } - trackSegmentEvent(SegmentEvent.vcsAction, { - ...vcsSegmentEventProperties('git', 'commit'), + trackAnalyticsEvent(AnalyticsEvent.vcsAction, { + ...vcsEventProperties('git', 'commit'), providerName, repoId: gitRepository._id, }); @@ -1730,8 +1730,8 @@ export const commitAndPushToGitRepoAction = async ({ providerName = credentials.provider; } - trackSegmentEvent(SegmentEvent.vcsAction, { - ...vcsSegmentEventProperties('git', 'commit'), + trackAnalyticsEvent(AnalyticsEvent.vcsAction, { + ...vcsEventProperties('git', 'commit'), providerName, repoId: repo._id, }); @@ -1770,8 +1770,8 @@ export const commitAndPushToGitRepoAction = async ({ try { await GitVCS.push(repo.credentialsId); - trackSegmentEvent(SegmentEvent.vcsAction, { - ...vcsSegmentEventProperties('git', 'push'), + trackAnalyticsEvent(AnalyticsEvent.vcsAction, { + ...vcsEventProperties('git', 'push'), providerName, repoId: repo._id, }); @@ -1804,8 +1804,8 @@ export const commitAndPushToGitRepoAction = async ({ } const errorMessage = getErrorMessage(err); - trackSegmentEvent(SegmentEvent.vcsAction, { - ...vcsSegmentEventProperties('git', 'push', errorMessage), + trackAnalyticsEvent(AnalyticsEvent.vcsAction, { + ...vcsEventProperties('git', 'push', errorMessage), providerName, repoId: repo._id, }); @@ -1846,8 +1846,8 @@ export const createNewGitBranchAction = async ({ providerName = credentials.provider; } await GitVCS.checkout(branch); - trackSegmentEvent(SegmentEvent.vcsAction, { - ...vcsSegmentEventProperties('git', 'create_branch'), + trackAnalyticsEvent(AnalyticsEvent.vcsAction, { + ...vcsEventProperties('git', 'create_branch'), providerName, repoId: gitRepository._id, }); @@ -2021,8 +2021,8 @@ export const mergeGitBranch = async ({ await repoFileWatcherRegistry.importAllFiles(gitRepoId); clearConflictSuppression(gitRepository._id); - trackSegmentEvent(SegmentEvent.vcsAction, { - ...vcsSegmentEventProperties('git', 'merge_branch'), + trackAnalyticsEvent(AnalyticsEvent.vcsAction, { + ...vcsEventProperties('git', 'merge_branch'), providerName, repoId: gitRepository._id, }); @@ -2050,8 +2050,8 @@ export const mergeGitBranch = async ({ errorMessage = `${err.message}, ${err.data.response}`; } - trackSegmentEvent(SegmentEvent.vcsAction, { - ...vcsSegmentEventProperties('git', 'merge_branch', errorMessage), + trackAnalyticsEvent(AnalyticsEvent.vcsAction, { + ...vcsEventProperties('git', 'merge_branch', errorMessage), providerName, repoId: gitRepository._id, }); @@ -2086,8 +2086,8 @@ export const deleteGitBranchAction = async ({ providerName = credentials.provider; } - trackSegmentEvent(SegmentEvent.vcsAction, { - ...vcsSegmentEventProperties('git', 'delete_branch'), + trackAnalyticsEvent(AnalyticsEvent.vcsAction, { + ...vcsEventProperties('git', 'delete_branch'), providerName, repoId: repo._id, }); @@ -2163,8 +2163,8 @@ export const pushToGitRemoteAction = async ({ const bufferId = await database.bufferChanges(); await GitVCS.push(gitRepository.credentialsId); - trackSegmentEvent(SegmentEvent.vcsAction, { - ...vcsSegmentEventProperties('git', force ? 'force_push' : 'push'), + trackAnalyticsEvent(AnalyticsEvent.vcsAction, { + ...vcsEventProperties('git', force ? 'force_push' : 'push'), providerName, repoId: gitRepository._id, }); @@ -2208,8 +2208,8 @@ export const pushToGitRemoteAction = async ({ } const errorMessage = getErrorMessage(err); - trackSegmentEvent(SegmentEvent.vcsAction, { - ...vcsSegmentEventProperties('git', 'push', errorMessage), + trackAnalyticsEvent(AnalyticsEvent.vcsAction, { + ...vcsEventProperties('git', 'push', errorMessage), providerName, repoId: gitRepository._id, }); @@ -2277,8 +2277,8 @@ export async function pullFromGitRemote({ projectId, workspaceId }: { projectId: await repoFileWatcherRegistry.importAllFiles(gitRepository._id); clearConflictSuppression(repoId); - trackSegmentEvent(SegmentEvent.vcsAction, { - ...vcsSegmentEventProperties('git', 'pull'), + trackAnalyticsEvent(AnalyticsEvent.vcsAction, { + ...vcsEventProperties('git', 'pull'), providerName: credentials.provider, repoId: gitRepository._id, }); @@ -2330,8 +2330,8 @@ export async function pullFromGitRemote({ projectId, workspaceId }: { projectId: providerName = credentials.provider; } - trackSegmentEvent(SegmentEvent.vcsAction, { - ...vcsSegmentEventProperties('git', 'pull', errorMessage), + trackAnalyticsEvent(AnalyticsEvent.vcsAction, { + ...vcsEventProperties('git', 'pull', errorMessage), providerName, repoId: gitRepository._id, }); @@ -2788,9 +2788,9 @@ async function completeSignInToGitProvider({ } if (isEditing) { - trackSegmentEvent(SegmentEvent.gitAuthenticationUpdated, { provider }); + trackAnalyticsEvent(AnalyticsEvent.gitAuthenticationUpdated, { provider }); } else { - trackSegmentEvent(SegmentEvent.gitAuthenticationCompleted, { provider }); + trackAnalyticsEvent(AnalyticsEvent.gitAuthenticationCompleted, { provider }); } return {}; diff --git a/packages/insomnia/src/main/install-plugin.ts b/packages/insomnia/src/main/install-plugin.ts index f9f5d53732..cea45872d0 100644 --- a/packages/insomnia/src/main/install-plugin.ts +++ b/packages/insomnia/src/main/install-plugin.ts @@ -7,7 +7,7 @@ import { promisify } from 'node:util'; import { app, net } from 'electron'; import { services } from '~/insomnia-data'; -import { SegmentEvent, trackSegmentEvent } from '~/main/analytics'; +import { AnalyticsEvent, trackAnalyticsEvent } from '~/main/analytics'; import { isDevelopment } from '../common/constants'; import { validatePluginName } from '../utils/plugin'; @@ -160,7 +160,7 @@ export default async function installPlugin(pluginName: string, allowScopedPacka }), ); - trackSegmentEvent(SegmentEvent.installPlugin, { + trackAnalyticsEvent(AnalyticsEvent.installPlugin, { pluginName: moduleName, pluginVersion: info.version, }); diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index e3c8344e29..64cd849ef5 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -192,7 +192,7 @@ export type MainOnChannels = | 'socketIO.event.on' | 'startExecution' | 'trackPageView' - | 'trackSegmentEvent' + | 'trackAnalyticsEvent' | 'updateLatestStepName' | 'webSocket.close' | 'webSocket.closeAll' diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index e32d45ae6e..bbc0f302b7 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -37,8 +37,8 @@ import type { import type { HiddenBrowserWindowBridgeAPI } from '../../entry.hidden-window'; import type { PluginTemplateTag, RenderedRequest } from '../../templating/types'; -import type { SegmentEvent } from '../analytics'; -import { setCurrentOrganizationId, trackPageView, trackSegmentEvent } from '../analytics'; +import type { AnalyticsEvent } from '../analytics'; +import { setCurrentOrganizationId, trackAnalyticsEvent, trackPageView } from '../analytics'; import { authorizeUserInDefaultBrowser, cancelAuthorizationInDefaultBrowser, @@ -186,7 +186,7 @@ export interface RendererToMainBridgeAPI { secretStorage: secretStorageBridgeAPI; electronStorage: electronStorageBridgeAPI; sync: SyncBridgeAPI; - trackSegmentEvent: (options: { event: string; properties?: Record }) => void; + trackAnalyticsEvent: (options: { event: string; properties?: Record }) => void; trackPageView: (options: { name: string }) => void; setCurrentOrganizationId: (organizationId: string | undefined) => void; showNunjucksContextMenu: (options: { @@ -413,8 +413,8 @@ export function registerMainHandlers() { cancelCurlRequest(requestId); }); - ipcMainOn('trackSegmentEvent', (_, options: { event: SegmentEvent; properties?: Record }): void => { - trackSegmentEvent(options.event, options.properties); + ipcMainOn('trackAnalyticsEvent', (_, options: { event: AnalyticsEvent; properties?: Record }): void => { + trackAnalyticsEvent(options.event, options.properties); }); ipcMainOn('trackPageView', (_, options: { name: string }): void => { trackPageView(options.name); diff --git a/packages/insomnia/src/main/llm-config-service.ts b/packages/insomnia/src/main/llm-config-service.ts index 42d7f8f4ce..1e17a800a7 100644 --- a/packages/insomnia/src/main/llm-config-service.ts +++ b/packages/insomnia/src/main/llm-config-service.ts @@ -4,7 +4,7 @@ import { app } from 'electron'; import { LLM_BACKENDS } from '~/common/constants'; import { services } from '~/insomnia-data'; -import { SegmentEvent, trackSegmentEvent } from '~/main/analytics'; +import { AnalyticsEvent, trackAnalyticsEvent } from '~/main/analytics'; import { ipcMainHandle } from '~/main/ipc/electron'; const LLM_PLUGIN_NAME = 'insomnia-llm'; @@ -123,7 +123,7 @@ export const getAIFeatureEnabled = async (feature: AIFeatureNames): Promise => { await services.pluginData.upsertByKey(LLM_PLUGIN_NAME, `feature.${feature}`, String(enabled)); - trackSegmentEvent(enabled ? SegmentEvent.aiFeatureEnabled : SegmentEvent.aiFeatureDisabled, { + trackAnalyticsEvent(enabled ? AnalyticsEvent.aiFeatureEnabled : AnalyticsEvent.aiFeatureDisabled, { feature: feature, set_for: 'user', }); diff --git a/packages/insomnia/src/main/mcp/client-requests.ts b/packages/insomnia/src/main/mcp/client-requests.ts index 4b3f6093d9..24d09d9777 100644 --- a/packages/insomnia/src/main/mcp/client-requests.ts +++ b/packages/insomnia/src/main/mcp/client-requests.ts @@ -11,7 +11,7 @@ import { } from '@modelcontextprotocol/sdk/types.js'; import { METHOD_SUBSCRIBE_RESOURCE, METHOD_UNSUBSCRIBE_RESOURCE } from '~/common/mcp-utils'; -import { SegmentEvent, trackSegmentEvent } from '~/main/analytics'; +import { AnalyticsEvent, trackAnalyticsEvent } from '~/main/analytics'; import { getActiveMcpClient, getReadyActiveMcpConnectionContext, writeEventLogAndNotify } from '~/main/mcp/common'; import type { CommonMcpOptions, McpMessageEventWithoutBase } from '~/main/mcp/types'; @@ -29,7 +29,7 @@ export const callTool = async (options: CommonMcpOptions & CallToolRequest['para const mcpClient = getActiveMcpClient(requestId); if (mcpClient) { const response = await mcpClient.callTool(params, CompatibilityCallToolResultSchema); - trackSegmentEvent(SegmentEvent.mcpToolCalled); + trackAnalyticsEvent(AnalyticsEvent.mcpToolCalled); return response.content; } return null; @@ -49,7 +49,7 @@ export const getPrompt = async (options: CommonMcpOptions & GetPromptRequest['pa const mcpClient = getActiveMcpClient(options.requestId); if (mcpClient) { const prompt = await mcpClient.getPrompt(params); - trackSegmentEvent(SegmentEvent.mcpPromptCalled); + trackAnalyticsEvent(AnalyticsEvent.mcpPromptCalled); return prompt; } return null; @@ -80,7 +80,7 @@ export const readResource = async (options: CommonMcpOptions & ReadResourceReque const mcpClient = getActiveMcpClient(requestId); if (mcpClient) { const resource = await mcpClient.readResource(params); - trackSegmentEvent(SegmentEvent.mcpResourceRead); + trackAnalyticsEvent(AnalyticsEvent.mcpResourceRead); return resource; } return null; diff --git a/packages/insomnia/src/main/network/mcp.ts b/packages/insomnia/src/main/network/mcp.ts index 28378a696d..c17dfbf78c 100644 --- a/packages/insomnia/src/main/network/mcp.ts +++ b/packages/insomnia/src/main/network/mcp.ts @@ -21,7 +21,7 @@ import electron from 'electron'; import { getAppVersion, getProductName, REALTIME_EVENTS_CHANNELS } from '~/common/constants'; import { getMcpMethodFromMessage, METHOD_NOTIFICATION_CANCELLED } from '~/common/mcp-utils'; import { models, services } from '~/insomnia-data'; -import { SegmentEvent, trackSegmentEvent } from '~/main/analytics'; +import { AnalyticsEvent, trackAnalyticsEvent } from '~/main/analytics'; import { callTool, getPrompt, @@ -257,7 +257,7 @@ const createTransportAndConnect = async (context: ConnectionContext, mcpClient: } const authDisabled = 'disabled' in mcpRequest.authentication && mcpRequest.authentication.disabled; const isFirstConnection = !mcpRequest.connected; - trackSegmentEvent(SegmentEvent.mcpClientConnected, { + trackAnalyticsEvent(AnalyticsEvent.mcpClientConnected, { transportType: connectionOptions.transportType, firstTime: isFirstConnection, ...(connectionOptions.transportType === models.mcpRequest.TRANSPORT_TYPES.HTTP @@ -455,7 +455,7 @@ const closeMcpConnection = async (options: CommonMcpOptions) => { // Execute clear resource subscription in main process rather than UI to make sure closeAllMcpConnections method will clear subscriptions await services.mcpRequest.clearResourceSubscriptions(requestId); } - trackSegmentEvent(SegmentEvent.mcpClientDisconnected); + trackAnalyticsEvent(AnalyticsEvent.mcpClientDisconnected); }; const closeAllMcpConnections = () => { diff --git a/packages/insomnia/src/root.tsx b/packages/insomnia/src/root.tsx index ea09034463..6561671d0f 100644 --- a/packages/insomnia/src/root.tsx +++ b/packages/insomnia/src/root.tsx @@ -35,7 +35,7 @@ import { GIT_PROVIDER_COMPLETE_SIGN_IN_FETCHER_KEY, useGitProviderCompleteSignInFetcher, } from '~/routes/git-credentials.complete-sign-in'; -import { PENDING_IMPORT_ATTRIBUTION_KEY, SegmentEvent, trackImportEvent } from '~/ui/analytics'; +import { AnalyticsEvent, PENDING_IMPORT_ATTRIBUTION_KEY, trackImportEvent } from '~/ui/analytics'; import { getLoginUrl } from '~/ui/auth-session-provider.client'; import { CopyButton } from '~/ui/components/base/copy-button'; import { Link } from '~/ui/components/base/link'; @@ -203,6 +203,11 @@ export const Layout = ({ children }: { children: React.ReactNode }) => { * insomnia://* ; + frame-src + blob: + * + insomnia://* + ; script-src 'self' 'unsafe-eval' @@ -386,10 +391,10 @@ const Root = () => { if (!userSession.id) { window.sessionStorage.setItem('pendingDeepLinkAfterAuthorize', url); window.localStorage.setItem('logoutMessage', 'Please log in to import this resource.'); - trackImportEvent(SegmentEvent.importLoginRequired); + trackImportEvent(AnalyticsEvent.importLoginRequired); return navigate(href('/auth/login')); } - trackImportEvent(SegmentEvent.importStarted, { source: 'import-url' }); + trackImportEvent(AnalyticsEvent.importStarted, { source: 'import-url' }); if (params.uri) { return setImportObject({ @@ -635,7 +640,7 @@ const Root = () => { if (pendingDeepLink && organizationId && organizationId !== models.organization.SCRATCHPAD_ORGANIZATION_ID) { window.sessionStorage.removeItem('pendingDeepLinkAfterAuthorize'); window.sessionStorage.setItem('suppressWelcomeModals', 'true'); - trackImportEvent(SegmentEvent.importResumedAfterLogin); + trackImportEvent(AnalyticsEvent.importResumedAfterLogin); window.main.openDeepLink(pendingDeepLink); } }, [organizationId]); diff --git a/packages/insomnia/src/routes/ai.generate-commit-messages.tsx b/packages/insomnia/src/routes/ai.generate-commit-messages.tsx index 1264f0c071..2db596ad68 100644 --- a/packages/insomnia/src/routes/ai.generate-commit-messages.tsx +++ b/packages/insomnia/src/routes/ai.generate-commit-messages.tsx @@ -1,6 +1,6 @@ import { href } from 'react-router'; -import { SegmentEvent } from '~/ui/analytics'; +import { AnalyticsEvent } from '~/ui/analytics'; import { showToast } from '~/ui/components/toast-notification'; import { createFetcherSubmitHook } from '~/utils/router'; @@ -38,8 +38,8 @@ export async function clientAction(args: Route.ClientActionArgs) { .join('\n'), }); - window.main.trackSegmentEvent({ - event: SegmentEvent.recommendCommitsGenerated, + window.main.trackAnalyticsEvent({ + event: AnalyticsEvent.recommendCommitsGenerated, properties: { file_count: commits?.map(commit => commit.files?.length || 0)?.reduce((a, b) => a + b, 0), group_count: commits?.length || 0, diff --git a/packages/insomnia/src/routes/auth.authorize.tsx b/packages/insomnia/src/routes/auth.authorize.tsx index e4e6f7255e..d15f20a581 100644 --- a/packages/insomnia/src/routes/auth.authorize.tsx +++ b/packages/insomnia/src/routes/auth.authorize.tsx @@ -4,7 +4,7 @@ import { Button, Heading } from 'react-aria-components'; import { href, redirect, useFetchers, useNavigate } from 'react-router'; import { services } from '~/insomnia-data'; -import { SegmentEvent } from '~/ui/analytics'; +import { AnalyticsEvent } from '~/ui/analytics'; import { getLoginUrl, submitAuthCode } from '~/ui/auth-session-provider.client'; import { Icon } from '~/ui/components/icon'; import { validateVaultKey } from '~/ui/vault-key.client'; @@ -32,8 +32,8 @@ export async function clientAction({ request }: Route.ClientActionArgs) { }; } console.log('Login successful'); - window.main.trackSegmentEvent({ - event: SegmentEvent.loginSuccess, + window.main.trackAnalyticsEvent({ + event: AnalyticsEvent.loginSuccess, }); window.localStorage.setItem('hasUserLoggedInBefore', 'true'); const userSession = await services.userSession.getOrCreate(); diff --git a/packages/insomnia/src/routes/auth.login.tsx b/packages/insomnia/src/routes/auth.login.tsx index fbd5439ca9..ad5651f5cf 100644 --- a/packages/insomnia/src/routes/auth.login.tsx +++ b/packages/insomnia/src/routes/auth.login.tsx @@ -3,7 +3,7 @@ import { Button } from 'react-aria-components'; import { href, redirect, useNavigate } from 'react-router'; import { models } from '~/insomnia-data'; -import { SegmentEvent } from '~/ui/analytics'; +import { AnalyticsEvent } from '~/ui/analytics'; import { getLoginUrl } from '~/ui/auth-session-provider.client'; import { Icon } from '~/ui/components/icon'; import { Tooltip } from '~/ui/components/tooltip'; @@ -152,8 +152,8 @@ const Component = () => {