feat: Add segment events [INS-5807] (#8802)

* fix: update segment event tracking for export completion

* fix: include platform information in segment event tracking properties

* feat: track segment events for import and export actions in workspace dropdown

* feat: track segment events for import and export actions in workspace dropdown

* feat: track segment event for export requests selection in modal

* feat: track segment event for import completion with workspace and request counts

* feat: include selected role in invitation tracking properties

* feat: track segment events for invite resent and revoked actions

* feat: track segment event for import action in project empty view

* feat: track segment event for scanned import source

* feat: track segment events for VCS pull and push actions with error handling

* feat: track segment event for import action initiation

* feat: track segment events for project creation and update actions with storage details

* feat: track segment event for import action initiation in project route

* fix: standardize storage property values to lowercase in project actions

* fix: remove unnecessary console log in ExportRequestsModal

* fix: replace alias import of database with direct import in actions file

* fix: update segment event source for import and export actions to include 'scratchpad'

* fix: restore import of showAlert in InviteModal component
This commit is contained in:
Pavlos Koutoglou
2025-07-01 11:14:48 +02:00
committed by GitHub
parent 2dd3dbe137
commit ee94938b40
15 changed files with 238 additions and 11 deletions

View File

@@ -6,8 +6,8 @@ import React from 'react';
import { type Environment } from '../models/environment';
import * as requestOperations from '../models/helpers/request-operations';
import { type BaseModel, environment } from '../models/index';
import * as models from '../models/index';
import { type BaseModel, environment } from '../models/index';
import { isRequest } from '../models/request';
import { isWorkspace, type Workspace } from '../models/workspace';
import { SegmentEvent } from '../ui/analytics';
@@ -268,7 +268,7 @@ export const exportProjectToFile = (activeProjectName: string, workspacesForActi
throw new Error(`selected export format "${selectedFormat}" is invalid`);
}
}
window.main.trackSegmentEvent({ event: SegmentEvent.dataExport, properties: { type: selectedFormat } });
window.main.trackSegmentEvent({ event: SegmentEvent.exportCompleted });
} catch (err) {
showError({
title: 'Export Failed',

View File

@@ -83,7 +83,10 @@ export async function trackSegmentEvent(event: SegmentEvent, properties?: Record
analytics.track(
{
event,
properties,
properties: {
...properties,
platform: 'app',
},
context,
anonymousId,
userId: userSession?.hashedAccountId || '',

View File

@@ -3,7 +3,11 @@ export enum SegmentEvent {
analyticsDisabled = 'Analytics Disabled',
collectionCreate = 'Collection Created',
dataExport = 'Data Exported',
exportCompleted = 'Export Completed',
dataImport = 'Data Imported',
importStarted = 'Import Started',
importScanned = 'Import Scanned',
importCompleted = 'Import Completed',
documentCreate = 'Document Created',
mockCreate = 'Mock Created',
environmentWorkspaceCreate = 'Environment Workspace Created',
@@ -33,6 +37,12 @@ export enum SegmentEvent {
vcsAction = 'VCS Action Executed',
buttonClick = 'Button Clicked',
inviteMember = 'Invite Member',
inviteResent = 'Invite Resent',
inviteRevoked = 'Invite Revoked',
projectCreated = 'Project Created',
projectUpdated = 'Project Updated',
exportStarted = 'Export Started',
exportRequestsChosen = 'Export Requests Chosen',
}
type PushPull = 'push' | 'pull';

View File

@@ -14,6 +14,7 @@ import { WorkspaceScopeKeys } from '../../../models/workspace';
import type { DocumentAction } from '../../../plugins';
import { getDocumentActions } from '../../../plugins';
import * as pluginContexts from '../../../plugins/context';
import { SegmentEvent } from '../../analytics';
import { useLoadingRecord } from '../../hooks/use-loading-record';
import { Dropdown, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
import { Icon } from '../icon';
@@ -143,13 +144,33 @@ export const WorkspaceCardDropdown: FC<Props> = props => {
</DropdownItem>
<DropdownSection aria-label="Meta section">
<DropdownItem aria-label="Import">
<ItemContent label="Import" icon="file-import" onClick={() => setIsImportModalOpen(true)} />
<ItemContent
label="Import"
icon="file-import"
onClick={() => {
window.main.trackSegmentEvent({
event: SegmentEvent.importStarted,
properties: {
source: `${workspace.scope}-list`,
},
});
setIsImportModalOpen(true);
}}
/>
</DropdownItem>
<DropdownItem aria-label="Export">
<ItemContent
label="Export"
icon="file-export"
onClick={() => {
window.main.trackSegmentEvent({
event: SegmentEvent.exportStarted,
properties: {
source: `${workspace.scope}-list`,
},
});
if (workspace.scope === 'mock-server') {
return exportMockServerToFile(workspace);
}

View File

@@ -29,6 +29,7 @@ import type { WorkspaceAction } from '../../../plugins';
import { getWorkspaceActions } from '../../../plugins';
import * as pluginContexts from '../../../plugins/context';
import { invariant } from '../../../utils/invariant';
import { SegmentEvent } from '../../analytics';
import { useAIContext } from '../../context/app/ai-context';
import { useRootLoaderData } from '../../routes/root';
import type { WorkspaceLoaderData } from '../../routes/workspace';
@@ -133,13 +134,28 @@ export const WorkspaceDropdown: FC<{}> = () => {
id: 'Import',
name: 'Import',
icon: <Icon icon="file-import" />,
action: () => setIsImportModalOpen(true),
action: () => {
window.main.trackSegmentEvent({
event: SegmentEvent.importStarted,
properties: {
source: `scratchpad-${activeWorkspace.scope}-menu`,
},
});
setIsImportModalOpen(true);
},
},
{
id: 'Export',
name: 'Export',
icon: <Icon icon="file-export" />,
action: () => {
window.main.trackSegmentEvent({
event: SegmentEvent.exportStarted,
properties: {
source: `scratchpad-${activeWorkspace.scope}-menu`,
},
});
if (activeWorkspace.scope === 'mock-server') {
return exportMockServerToFile(activeWorkspace);
}
@@ -176,7 +192,15 @@ export const WorkspaceDropdown: FC<{}> = () => {
id: 'from-file',
name: 'From File',
icon: <Icon icon="file-import" />,
action: () => setIsImportModalOpen(true),
action: () => {
window.main.trackSegmentEvent({
event: SegmentEvent.importStarted,
properties: {
source: `${activeWorkspace.scope}-menu`,
},
});
setIsImportModalOpen(true);
},
},
],
},
@@ -235,6 +259,13 @@ export const WorkspaceDropdown: FC<{}> = () => {
name: 'Export',
icon: <Icon icon="file-export" />,
action: () => {
window.main.trackSegmentEvent({
event: SegmentEvent.exportStarted,
properties: {
source: `${activeWorkspace.scope}-menu`,
},
});
if (activeWorkspace.scope === 'mock-server') {
return exportMockServerToFile(activeWorkspace);
}

View File

@@ -8,6 +8,7 @@ import { type GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
import { isRequest, type Request } from '../../../models/request';
import type { RequestGroup } from '../../../models/request-group';
import { isWebSocketRequest, type WebSocketRequest } from '../../../models/websocket-request';
import { SegmentEvent } from '../../analytics';
import type { Child, WorkspaceLoaderData } from '../../routes/workspace';
import { Icon } from '../icon';
import { getMethodShortHand } from '../tags/method-tag';
@@ -330,6 +331,15 @@ export const ExportRequestsModal = ({
</Button>
<Button
onPress={() => {
if (state?.treeRoot) {
window.main.trackSegmentEvent({
event: SegmentEvent.exportRequestsChosen,
properties: {
totalRequests: state.treeRoot.totalRequests,
exported_requests: state.treeRoot.selectedRequests,
},
});
}
state?.treeRoot && exportRequestsToFile(workspaceIdToExport, getSelectedRequestIds(state.treeRoot));
close();
}}

View File

@@ -333,9 +333,20 @@ export const ImportModal: FC<ImportModalProps> = ({
useEffect(() => {
if (importFetcher?.data?.done === true) {
// Track the import completion event
if (scanResourcesFetcherData?.length) {
window.main.trackSegmentEvent({
event: SegmentEvent.importCompleted,
properties: {
workspaces: scanResourcesFetcherData.map(scanResult => scanResult.workspaces?.length || 0),
requests: scanResourcesFetcherData.map(scanResult => scanResult.requests?.length || 0),
},
});
}
modalRef.current?.hide();
}
}, [importFetcher.data]);
}, [importFetcher.data, scanResourcesFetcherData]);
// allow workspace import if there is only one workspace
const totalWorkspacesCount = useMemo(() => {
return (

View File

@@ -259,6 +259,7 @@ export const InviteForm = ({ allRoles, onInviteCompleted }: EmailsInputProps) =>
properties: {
numberOfInvites: emailsToInvite.length,
numberOfTeams: groupsToInvite.length,
role: selectedRoleRef.current.name,
},
});

View File

@@ -23,7 +23,7 @@ import { insomniaFetch } from '../../../insomniaFetch';
import type { Collaborator, CollaboratorsListLoaderResult } from '../../../routes/invite';
import { PromptButton } from '../../base/prompt-button';
import { Icon } from '../../icon';
import { showAlert } from '..';
import { showAlert } from '../index';
import { InviteForm } from './invite-form';
import { OrganizationMemberRolesSelector, type Role, SELECTOR_TYPE } from './organization-member-roles-selector';
@@ -373,6 +373,7 @@ const MemberListItem: FC<{
method: 'POST',
},
);
window.main.trackSegmentEvent({ event: SegmentEvent.inviteResent });
}
}}
className="flex min-w-[75px] items-center gap-2 px-2 py-1 text-sm font-semibold text-[--color-font] transition-all aria-pressed:bg-[--hl-sm]"
@@ -476,6 +477,7 @@ const MemberListItem: FC<{
revokeOrganizationInvite(organizationId, member.metadata.invitationId)
.then(() => {
onResetCurrentPage();
window.main.trackSegmentEvent({ event: SegmentEvent.inviteRevoked });
})
.catch(error => {
onError(error.message);

View File

@@ -1,6 +1,7 @@
import React, { type FC } from 'react';
import { Button } from 'react-aria-components';
import { SegmentEvent } from '../../analytics';
import { Icon } from '../icon';
interface Props {
@@ -38,7 +39,16 @@ export const ProjectEmptyView: FC<Props> = ({
<Button
aria-label="Import"
className="flex w-full max-w-[180px] flex-col items-center justify-center gap-[var(--padding-xs)] rounded-md border border-solid border-[--hl-sm] px-12 py-8 text-[var(--font-size-sm)] shadow-sm transition-all duration-100 hover:bg-[--color-bg] sm:gap-[var(--padding-sm)]"
onPress={onImportFrom}
onPress={() => {
window.main.trackSegmentEvent({
event: SegmentEvent.importStarted,
properties: {
source: 'home-page',
},
});
onImportFrom();
}}
>
<Icon icon="file-import" className="text-[var(--font-size-xl)]" />
Import

View File

@@ -9,7 +9,6 @@ import { version } from '../../../package.json';
import { parseApiSpec, resolveComponentSchemaRefs } from '../../common/api-specs';
import { ACTIVITY_DEBUG, getAIServiceURL, METHOD_GET } from '../../common/constants';
import { database } from '../../common/database';
import { database as db } from '../../common/database';
import { importResourcesToWorkspace, scanResources, type ScanResult } from '../../common/import';
import { generateId } from '../../common/misc';
import * as models from '../../models';
@@ -39,6 +38,7 @@ import { SpectralRunner } from '../worker/spectral-handler';
// Project
export const createNewProjectAction: ActionFunction = async ({ request, params }) => {
const { organizationId } = params;
invariant(organizationId, 'Organization ID is required');
const newProjectData = (await request.json()) as {
name: string;
@@ -62,6 +62,13 @@ export const createNewProjectAction: ActionFunction = async ({ request, params }
parentId: organizationId,
});
window.main.trackSegmentEvent({
event: SegmentEvent.projectCreated,
properties: {
storage: 'local',
},
});
return redirect(`/organization/${organizationId}/project/${project._id}`);
}
@@ -77,6 +84,13 @@ export const createNewProjectAction: ActionFunction = async ({ request, params }
};
}
window.main.trackSegmentEvent({
event: SegmentEvent.projectCreated,
properties: {
storage: 'git',
},
});
return redirect(`/organization/${organizationId}/project/${projectId}`);
}
@@ -99,6 +113,15 @@ export const createNewProjectAction: ActionFunction = async ({ request, params }
sessionId,
});
if (newCloudProject && !('error' in newCloudProject)) {
window.main.trackSegmentEvent({
event: SegmentEvent.projectCreated,
properties: {
storage: 'remote',
},
});
}
if (!newCloudProject || 'error' in newCloudProject) {
let error = 'An unexpected error occurred while creating the project. Please try again.';
if (newCloudProject.error === 'FORBIDDEN') {
@@ -209,6 +232,15 @@ export const updateProjectAction: ActionFunction = async ({ request, params }) =
sessionId,
});
if (response && !response.error) {
window.main.trackSegmentEvent({
event: SegmentEvent.projectUpdated,
properties: {
storage: 'local',
},
});
}
if (response && 'error' in response) {
let error = 'An unexpected error occurred while updating your project. Please try again.';
@@ -250,6 +282,15 @@ export const updateProjectAction: ActionFunction = async ({ request, params }) =
sessionId,
});
if (newCloudProject && !('error' in newCloudProject)) {
window.main.trackSegmentEvent({
event: SegmentEvent.projectUpdated,
properties: {
storage: 'remote',
},
});
}
if (!newCloudProject || 'error' in newCloudProject) {
let error = 'An unexpected error occurred while updating your project. Please try again.';
if (newCloudProject.error === 'FORBIDDEN') {
@@ -293,6 +334,15 @@ export const updateProjectAction: ActionFunction = async ({ request, params }) =
sessionId,
});
if (response && !response.error) {
window.main.trackSegmentEvent({
event: SegmentEvent.projectUpdated,
properties: {
storage: 'git',
},
});
}
if (response && 'error' in response) {
let error = 'An unexpected error occurred while updating your project. Please try again.';
@@ -358,6 +408,13 @@ export const updateProjectAction: ActionFunction = async ({ request, params }) =
// local project rename
await models.project.update(project, { name });
window.main.trackSegmentEvent({
event: SegmentEvent.projectUpdated,
properties: {
storage: 'local',
},
});
return {
success: true,
};
@@ -683,7 +740,7 @@ async function duplicateWorkspace(
invariant(workspace, 'Workspace not found');
invariant(duplicateToProject, 'Project not found');
async function duplicate(workspace: Workspace, { name, parentId }: Pick<Workspace, 'name' | 'parentId'>) {
const newWorkspace = await db.duplicate(workspace, {
const newWorkspace = await database.duplicate(workspace, {
name,
parentId,
});

View File

@@ -21,6 +21,7 @@ import {
import { VCSInstance } from '../../sync/vcs/insomnia-sync';
import type { ImportEntry } from '../../utils/importers/entities';
import { invariant } from '../../utils/invariant';
import { SegmentEvent } from '../analytics';
import { fetchAndCacheOrganizationStorageRule } from './organization';
export const scanForResourcesAction: ActionFunction = async ({ request }): Promise<ScanResult[]> => {
@@ -31,6 +32,13 @@ export const scanForResourcesAction: ActionFunction = async ({ request }): Promi
invariant(typeof source === 'string', 'Source is required.');
invariant(['file', 'uri', 'clipboard'].includes(source), 'Unsupported import type');
window.main.trackSegmentEvent({
event: SegmentEvent.importScanned,
properties: {
source,
},
});
const contentList: ImportEntry[] = [];
if (source === 'uri') {
const uri = formData.get('uri');
@@ -150,6 +158,7 @@ export const importResourcesAction: ActionFunction = async ({ request }): Promis
await importResourcesToWorkspace({
workspaceId: workspaceId,
});
// TODO: find more elegant way to wait for import to finish
return { done: true };
}

View File

@@ -66,6 +66,7 @@ import { VCSInstance } from '../../sync/vcs/insomnia-sync';
import { insomniaFetch } from '../../ui/insomniaFetch';
import { invariant } from '../../utils/invariant';
import { getInitialRouteForOrganization } from '../../utils/router';
import { SegmentEvent } from '../analytics';
import { AvatarGroup } from '../components/avatar';
import { GitProjectSyncDropdown } from '../components/dropdowns/git-project-sync-dropdown';
import { ProjectDropdown } from '../components/dropdowns/project-dropdown';
@@ -1308,6 +1309,12 @@ const ProjectRoute: FC = () => {
<Button
onPress={() => {
window.main.trackSegmentEvent({
event: SegmentEvent.importStarted,
properties: {
source: 'project',
},
});
setImportModalType('file');
}}
aria-label="Import"

View File

@@ -19,6 +19,30 @@ import type { BackendProject, Compare, Snapshot, Status, StatusCandidate } from
import { UserAbortResolveMergeConflictError, VCSInstance } from '../../sync/vcs/insomnia-sync';
import { pullBackendProject } from '../../sync/vcs/pull-backend-project';
import { invariant } from '../../utils/invariant';
import { SegmentEvent } from '../analytics';
type PushPull = 'push' | 'pull';
type VCSAction =
| PushPull
| `force_${PushPull}`
| 'create_branch'
| 'merge_branch'
| 'delete_branch'
| 'checkout_branch'
| 'commit'
| 'stage_all'
| 'stage'
| 'unstage_all'
| 'unstage'
| 'rollback'
| 'rollback_all'
| 'update'
| 'setup'
| 'clone';
export function vcsSegmentEventProperties(type: 'remote', action: VCSAction, error?: string) {
return { type, action, error };
}
async function getSyncItems({ workspaceId }: { workspaceId: string }) {
const syncItemsList: (
@@ -457,10 +481,21 @@ export const pullFromRemoteAction: ActionFunction = async ({ params }) => {
projectId: project._id,
});
window.main.trackSegmentEvent({
event: SegmentEvent.vcsAction,
properties: vcsSegmentEventProperties('remote', 'pull'),
});
await database.batchModifyDocs(delta);
delete remoteCompareCache[workspaceId];
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error while pulling from remote.';
window.main.trackSegmentEvent({
event: SegmentEvent.vcsAction,
properties: vcsSegmentEventProperties('remote', 'pull', errorMessage),
});
return {
error: errorMessage,
};
@@ -519,9 +554,21 @@ export const pushToRemoteAction: ActionFunction = async ({ params }) => {
teamId: project.parentId,
teamProjectId: project.remoteId,
});
window.main.trackSegmentEvent({
event: SegmentEvent.vcsAction,
properties: vcsSegmentEventProperties('remote', 'push'),
});
delete remoteCompareCache[workspaceId];
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error while pushing to remote.';
window.main.trackSegmentEvent({
event: SegmentEvent.vcsAction,
properties: vcsSegmentEventProperties('remote', 'push', errorMessage),
});
return {
error: errorMessage,
};

View File

@@ -11,6 +11,7 @@ import type { UserSession } from '../../models/user-session';
import { reloadPlugins } from '../../plugins';
import { createPlugin } from '../../plugins/create';
import { setTheme } from '../../plugins/misc';
import { SegmentEvent } from '../analytics';
import { getLoginUrl } from '../auth-session-provider';
import { ErrorBoundary } from '../components/error-boundary';
import { showError, showModal } from '../components/modals';
@@ -90,6 +91,13 @@ const Root = () => {
);
}
if (urlWithoutParams === 'insomnia://app/import') {
window.main.trackSegmentEvent({
event: SegmentEvent.importStarted,
properties: {
source: 'import-url',
},
});
return setImportUri(params.uri);
}
if (urlWithoutParams === 'insomnia://plugins/install') {