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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/insomnia-inso/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"shellwords": "^1.0.1"
},
"dependencies": {
"@segment/analytics-node": "^2.2.1",
"@seald-io/nedb": "^4.1.1",
"@stoplight/spectral-core": "^1.20.0",
"@stoplight/spectral-formats": "^1.8.2",
Expand Down
37 changes: 37 additions & 0 deletions packages/insomnia-inso/src/analytics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

const mockTrack = vi.fn();

vi.mock('@segment/analytics-node', () => ({
Analytics: vi.fn(() => ({
track: mockTrack,
closeAndFlush: vi.fn(),
})),
}));

vi.mock('./db/adapters/ne-db-adapter', () => ({
default: vi.fn().mockResolvedValue(null),
}));

describe('analytics', () => {
beforeEach(() => {
vi.stubEnv('NODE_ENV', 'production');
vi.resetModules();
mockTrack.mockClear();
});

it('should use the same anonymousId for multiple trackInsoEvent calls', async () => {
const { trackInsoEvent, InsoEvent } = await import('./analytics');

await trackInsoEvent(InsoEvent.lintSpec);
await trackInsoEvent(InsoEvent.exportSpec);

expect(mockTrack).toHaveBeenCalledTimes(2);

const firstCallAnonymousId = mockTrack.mock.calls[0][0].anonymousId;
const secondCallAnonymousId = mockTrack.mock.calls[1][0].anonymousId;

expect(firstCallAnonymousId).toBe(secondCallAnonymousId);
expect(firstCallAnonymousId).toMatch(/^anon_/);
});
});
118 changes: 118 additions & 0 deletions packages/insomnia-inso/src/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import os from 'node:os';

import { Analytics } from '@segment/analytics-node';
import { getSegmentWriteKey } from 'insomnia/src/common/constants';
import type { Settings } from 'insomnia/src/models/settings';
import { v4 as uuidv4 } from 'uuid';

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',
}

const analyticsClient = new Analytics({ writeKey: getSegmentWriteKey() });
let deviceId: string | null = null;
let localSettings: Settings | null = null;

const getLocalSettings = async (): Promise<Settings | null> => {
if (localSettings) {
return localSettings;
}

try {
const appDataDir = getAppDataDir(getDefaultProductName());
const db = await neDbAdapter(appDataDir, ['Settings']);
localSettings = db?.Settings?.[0] ?? null;
return localSettings;
} catch {
return null;
}
};

const getDeviceId = async (): Promise<string> => {
if (deviceId) {
return deviceId;
}

try {
const settings = await getLocalSettings();
if (settings?.deviceId) {
deviceId = settings.deviceId;
return deviceId;
}
} catch {}

deviceId = `anon_${uuidv4()}`;
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<string, unknown>): Promise<void> => {
if (process.env.NODE_ENV === 'test') {
return;
}

const settings = await getLocalSettings();
if (settings && !settings.enableAnalytics) {
return;
}

try {
const anonymousId = await getDeviceId();
const version = process.env.VERSION || packageJson.version;

analyticsClient.track(
{
event,
anonymousId,
properties: {
...properties,
platform: 'cli',
},
context: {
app: {
name: 'inso',
version,
},
os: {
name: getOsName(),
version: os.release(),
},
},
},
() => {
// Silently fail
},
);
} catch {
// Silently fail
}
};

export const flushAnalytics = async (): Promise<void> => {
try {
await analyticsClient.closeAndFlush({ timeout: 5000 });
} catch {
// Silently fail
}
};
82 changes: 40 additions & 42 deletions packages/insomnia-inso/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import fs from 'node:fs';
import { readFile } from 'node:fs/promises';
import { homedir } from 'node:os';
import nodePath from 'node:path';

import * as commander from 'commander';
Expand All @@ -26,10 +25,10 @@ import type { Workspace } from '~/models/workspace';

import type { RequestTestResult } from '../../insomnia-scripting-environment/src/objects';
import packageJson from '../package.json';
import { flushAnalytics, InsoEvent, trackInsoEvent } from './analytics';
import { exportSpecification, writeFileWithCliOptions } from './commands/export-specification';
import { getRuleSetFileFromFolderByFilename, lintSpecification } from './commands/lint-specification';
import { RunCollectionResultReport } from './commands/run-collection/result-report';
import type { Database } from './db';
import { isFile, loadDb } from './db';
import { insomniaExportAdapter } from './db/adapters/insomnia-adapter';
import { loadApiSpec, promptApiSpec } from './db/models/api-spec';
Expand All @@ -38,9 +37,12 @@ import type { BaseModel } from './db/models/types';
import { loadTestSuites, promptTestSuites } from './db/models/unit-test-suite';
import { matchIdIsh } from './db/models/util';
import { loadWorkspace, promptWorkspace } from './db/models/workspace';
import { BasicReporter, logger, LogLevel } from './logger';
import type { Database } from './db/types';
import { InsoError } from './errors';
import { BasicReporter, logger,LogLevel } from './logger';
import { logTestResult, logTestResultSummary, reporterTypes, type TestReporter } from './reporter';
import { generateDocumentation } from './scripts/docs';
import { getAppDataDir, getDefaultProductName } from './util';

export interface GlobalOptions {
ci: boolean;
Expand Down Expand Up @@ -89,45 +91,6 @@ export const tryToReadInsoConfigFile = async (configFile?: string, workingDir?:
return {};
};

export class InsoError extends Error {
cause?: Error | null;

constructor(message: string, cause?: Error) {
super(message);
this.name = 'InsoError';
this.cause = cause;
}
}

/**
* getAppDataDir returns the data directory for an Electron app,
* it is equivalent to the app.getPath('userData') API in Electron.
* https://www.electronjs.org/docs/api/app#appgetpathname
*/
export function getAppDataDir(app: string): string {
switch (process.platform) {
case 'darwin': {
return nodePath.join(homedir(), 'Library', 'Application Support', app);
}
case 'win32': {
return nodePath.join(process.env.APPDATA || nodePath.join(homedir(), 'AppData', 'Roaming'), app);
}
case 'linux': {
return nodePath.join(process.env.XDG_DATA_HOME || nodePath.join(homedir(), '.config'), app);
}
default: {
throw new Error('Unsupported platform');
}
}
}
export const getDefaultProductName = (): string => {
const name = process.env.DEFAULT_APP_NAME;
if (!name) {
throw new Error('Environment variable DEFAULT_APP_NAME is not set.');
}
return name;
};

const localAppDir = getAppDataDir(getDefaultProductName());

export const getAbsoluteFilePath = ({ workingDir, file }: { workingDir?: string; file: string }) => {
Expand Down Expand Up @@ -504,8 +467,15 @@ export const go = (args?: string[]) => {

// TODO: is this necessary?
const success = options.verbose ? await runTestPromise : await noConsoleLog(() => runTestPromise);

await trackInsoEvent(InsoEvent.runTest, { success });
await flushAnalytics();

return process.exit(success ? 0 : 1);
} catch (error) {
await trackInsoEvent(InsoEvent.runTest, { success: false });
await flushAnalytics();

logErrorAndExit(error);
}
return process.exit(1);
Expand Down Expand Up @@ -892,11 +862,18 @@ export const go = (args?: string[]) => {
logTestResultSummary(testResultsQueue);

await report.saveReport();

await trackInsoEvent(InsoEvent.runCollection, { success });
await flushAnalytics();

return process.exit(success ? 0 : 1);
} catch (error) {
report.update({ error: (error instanceof Error ? error.message : String(error)) || 'Unknown error' });
await report.saveReport();

await trackInsoEvent(InsoEvent.runCollection, { success: false });
await flushAnalytics();

logErrorAndExit(error);
}
return process.exit(1);
Expand Down Expand Up @@ -948,8 +925,15 @@ export const go = (args?: string[]) => {

try {
const { isValid } = await lintSpecification({ specContent, rulesetFileName });

await trackInsoEvent(InsoEvent.lintSpec, { success: isValid });
await flushAnalytics();

return process.exit(isValid ? 0 : 1);
} catch (error) {
await trackInsoEvent(InsoEvent.lintSpec, { success: false });
await flushAnalytics();

logErrorAndExit(error);
}
return process.exit(1);
Expand Down Expand Up @@ -980,12 +964,23 @@ export const go = (args?: string[]) => {
options.output && getAbsoluteFilePath({ workingDir: options.workingDir, file: options.output });
if (!outputPath) {
logger.log(toExport);

await trackInsoEvent(InsoEvent.exportSpec, { success: true });
await flushAnalytics();

return process.exit(0);
}
const filePath = await writeFileWithCliOptions(outputPath, toExport);
logger.log(`Specification exported to "${filePath}".`);

await trackInsoEvent(InsoEvent.exportSpec, { success: true });
await flushAnalytics();

return process.exit(0);
} catch (error) {
await trackInsoEvent(InsoEvent.exportSpec, { success: false });
await flushAnalytics();

logErrorAndExit(error);
}
return process.exit(1);
Expand Down Expand Up @@ -1020,6 +1015,9 @@ export const go = (args?: string[]) => {

logger.debug(`>> ${scriptArgs.slice(1).join(' ')}`);

// Track script invocation - the underlying command will track its own success/failure
await trackInsoEvent(InsoEvent.script);

program.parseAsync(scriptArgs).catch(logErrorAndExit);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'node:path';

import YAML from 'yaml';

import { InsoError } from '../cli';
import { InsoError } from '../errors';

export async function writeFileWithCliOptions(outputPath: string, contents: string): Promise<string> {
try {
Expand Down
2 changes: 1 addition & 1 deletion packages/insomnia-inso/src/commands/lint-specification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import path from 'node:path';
import { oas } from '@stoplight/spectral-rulesets';
import { DiagnosticSeverity } from '@stoplight/types';

import { InsoError } from '../cli';
import { InsoError } from '../errors';
import { logger } from '../logger';
export const getRuleSetFileFromFolderByFilename = async (filePath: string) => {
try {
Expand Down
4 changes: 2 additions & 2 deletions packages/insomnia-inso/src/db/adapters/git-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import path from 'node:path';

import YAML from 'yaml';

import type { Database, DbAdapter } from '../index';
import { emptyDb } from '../index';
import type { Database, DbAdapter } from '../types';
import { emptyDb } from '../types';

const gitAdapter: DbAdapter = async (dir, filterTypes) => {
// Confirm if model directories exist
Expand Down
6 changes: 3 additions & 3 deletions packages/insomnia-inso/src/db/adapters/insomnia-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import path from 'node:path';
import { importInsomniaV5Data } from 'insomnia/src/common/insomnia-v5';
import YAML from 'yaml';

import { InsoError } from '../../cli';
import type { DbAdapter } from '../index';
import { emptyDb } from '../index';
import { InsoError } from '../../errors';
import type { BaseModel } from '../models/types';
import type { DbAdapter } from '../types';
import { emptyDb } from '../types';

/**
* When exporting from Insomnia, the `models.[kind].type` is converted from PascalCase to snake_case.
Expand Down
4 changes: 2 additions & 2 deletions packages/insomnia-inso/src/db/adapters/ne-db-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import path from 'node:path';

import NeDB from '@seald-io/nedb';

import type { Database, DbAdapter } from '../index';
import { emptyDb } from '../index';
import type { BaseModel } from '../models/types';
import type { Database, DbAdapter } from '../types';
import { emptyDb } from '../types';

const neDbAdapter: DbAdapter = async (dir, filterTypes) => {
// Confirm if db files exist
Expand Down
Loading
Loading