chore: move GA events to segment (#4403)

* drop events

* move pageview to segment

* replace trackEvent with trackSegmentEvent

* remove categories and set ids

* remove duplicate events

* fxi tests

* fix lint

* fix inso

* use analytics setting and dont log boring stuff

* add context to page view
This commit is contained in:
Jack Kavanagh 2022-01-25 15:50:46 +01:00 committed by GitHub
parent 3c66874414
commit 48ac330d95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 100 additions and 614 deletions

View File

@ -1,5 +1,4 @@
// WARNING: changing this to `export default` will break the mock and be incredibly hard to debug. Ask me how I know.
const _analytics = jest.requireActual('../analytics');
_analytics.trackEvent = jest.fn();
_analytics.trackSegmentEvent = jest.fn();
module.exports = _analytics;

View File

@ -1,203 +0,0 @@
import * as electron from 'electron';
import { EventEmitter } from 'events';
import { globalBeforeEach } from '../../__jest__/before-each';
import * as models from '../../models/index';
import { _trackEvent, _trackPageView } from '../analytics';
import {
getAppId,
getAppName,
getAppPlatform,
getAppVersion,
getBrowserUserAgent,
getGoogleAnalyticsId,
getGoogleAnalyticsLocation,
} from '../constants';
describe('init()', () => {
beforeEach(async () => {
await globalBeforeEach();
electron.net.request = jest.fn(() => {
const req = new EventEmitter();
req.end = function() {};
return req;
});
jest.useFakeTimers();
});
it('does not work with tracking disabled', async () => {
const settings = await models.settings.patch({
enableAnalytics: false,
deviceId: 'device',
});
expect(settings.enableAnalytics).toBe(false);
expect(electron.net.request.mock.calls).toEqual([]);
await _trackEvent({ interactive: true, category: 'Foo', action: 'Bar' });
jest.runAllTimers();
expect(electron.net.request.mock.calls).toEqual([]);
});
it('works with tracking enabled', async () => {
const settings = await models.settings.patch({
enableAnalytics: true,
deviceId: 'device',
});
expect(settings.enableAnalytics).toBe(true);
expect(electron.net.request.mock.calls).toEqual([]);
await _trackEvent({ interactive: true, category: 'Foo', action: 'Bar' });
jest.runAllTimers();
expect(electron.net.request.mock.calls).toEqual([
[
'https://www.google-analytics.com/collect?' +
'v=1&' +
`tid=${getGoogleAnalyticsId()}&` +
'cid=device&' +
`ua=${getBrowserUserAgent()}&` +
`dl=${encodeURIComponent(getGoogleAnalyticsLocation())}%2F&` +
'sr=1920x1080&' +
'ul=en-US&' +
`dt=${getAppId()}%3A${getAppVersion()}&` +
`cd1=${getAppPlatform()}&` +
`cd2=${getAppVersion()}&` +
'aip=1&' +
`an=${encodeURI(getAppName())}&` +
`aid=${getAppId()}&` +
`av=${getAppVersion()}&` +
'vp=1900x1060&' +
'de=UTF-8&' +
't=event&' +
'ec=Foo&' +
'ea=Bar',
],
]);
});
it('tracks non-interactive event', async () => {
await models.settings.patch({
deviceId: 'device',
enableAnalytics: true,
});
await _trackEvent({ interactive: false, category: 'Foo', action: 'Bar' });
jest.runAllTimers();
expect(electron.net.request.mock.calls).toEqual([
[
'https://www.google-analytics.com/collect?' +
'v=1&' +
`tid=${getGoogleAnalyticsId()}&` +
'cid=device&' +
`ua=${getBrowserUserAgent()}&` +
`dl=${encodeURIComponent(getGoogleAnalyticsLocation())}%2F&` +
'sr=1920x1080&' +
'ul=en-US&' +
`dt=${getAppId()}%3A${getAppVersion()}&` +
`cd1=${getAppPlatform()}&` +
`cd2=${getAppVersion()}&` +
'aip=1&' +
`an=${encodeURI(getAppName())}&` +
`aid=${getAppId()}&` +
`av=${getAppVersion()}&` +
'vp=1900x1060&' +
'de=UTF-8&' +
't=event&' +
'ec=Foo&' +
'ea=Bar&' +
'ni=1',
],
]);
});
it('tracks page view', async () => {
await models.settings.patch({
deviceId: 'device',
enableAnalytics: true,
});
await _trackPageView('/my/path');
jest.runAllTimers();
expect(electron.net.request.mock.calls).toEqual([
[
'https://www.google-analytics.com/collect?' +
'v=1&' +
`tid=${getGoogleAnalyticsId()}&` +
'cid=device&' +
`ua=${getBrowserUserAgent()}&` +
`dl=${encodeURIComponent(getGoogleAnalyticsLocation())}%2Fmy%2Fpath&` +
'sr=1920x1080&' +
'ul=en-US&' +
`dt=${getAppId()}%3A${getAppVersion()}&` +
`cd1=${getAppPlatform()}&` +
`cd2=${getAppVersion()}&` +
'aip=1&' +
`an=${encodeURI(getAppName())}&` +
`aid=${getAppId()}&` +
`av=${getAppVersion()}&` +
'vp=1900x1060&' +
'de=UTF-8&' +
't=pageview',
],
]);
});
it('tracking page view remembers path', async () => {
await models.settings.patch({
deviceId: 'device',
enableAnalytics: true,
});
await _trackPageView('/my/path');
jest.runAllTimers();
await _trackEvent({
interactive: true,
category: 'cat',
action: 'act',
label: 'lab',
value: 'val',
});
expect(electron.net.request.mock.calls).toEqual([
[
'https://www.google-analytics.com/collect?' +
'v=1&' +
`tid=${getGoogleAnalyticsId()}&` +
'cid=device&' +
`ua=${getBrowserUserAgent()}&` +
`dl=${encodeURIComponent(getGoogleAnalyticsLocation())}%2Fmy%2Fpath&` +
'sr=1920x1080&' +
'ul=en-US&' +
`dt=${getAppId()}%3A${getAppVersion()}&` +
`cd1=${getAppPlatform()}&` +
`cd2=${getAppVersion()}&` +
'aip=1&' +
`an=${encodeURI(getAppName())}&` +
`aid=${getAppId()}&` +
`av=${getAppVersion()}&` +
'vp=1900x1060&' +
'de=UTF-8&' +
't=pageview',
],
[
'https://www.google-analytics.com/collect?' +
'v=1&' +
`tid=${getGoogleAnalyticsId()}&` +
'cid=device&' +
`ua=${getBrowserUserAgent()}&` +
`dl=${encodeURIComponent(getGoogleAnalyticsLocation())}%2Fmy%2Fpath&` +
'sr=1920x1080&' +
'ul=en-US&' +
`dt=${getAppId()}%3A${getAppVersion()}&` +
`cd1=${getAppPlatform()}&` +
`cd2=${getAppVersion()}&` +
'aip=1&' +
`an=${encodeURI(getAppName())}&` +
`aid=${getAppId()}&` +
`av=${getAppVersion()}&` +
'vp=1900x1060&' +
'de=UTF-8&' +
't=event&' +
'ec=cat&' +
'ea=act&' +
'el=lab&' +
'ev=val',
],
]);
});
});

View File

@ -1,95 +1,58 @@
import Analytics from 'analytics-node';
import * as electron from 'electron';
import { buildQueryStringFromParams, joinUrlAndQueryString } from 'insomnia-url';
import * as uuid from 'uuid';
import { getAccountId } from '../account/session';
import { database as db } from '../common/database';
import * as models from '../models/index';
import type { RequestParameter } from '../models/request';
import { isSettings } from '../models/settings';
import {
getAppId,
getAppName,
getAppPlatform,
getAppVersion,
getGoogleAnalyticsId,
getGoogleAnalyticsLocation,
getSegmentWriteKey,
isDevelopment,
} from './constants';
import { getScreenResolution, getUserLanguage, getViewportSize } from './electron-helpers';
const DIMENSION_PLATFORM = 1;
const DIMENSION_VERSION = 2;
const KEY_TRACKING_ID = 'tid';
const KEY_VERSION = 'v';
const KEY_CLIENT_ID = 'cid';
const KEY_HIT_TYPE = 't';
const KEY_LOCATION = 'dl';
const KEY_TITLE = 'dt';
const KEY_NON_INTERACTION = 'ni';
const KEY_VIEWPORT_SIZE = 'vp';
const KEY_SCREEN_RESOLUTION = 'sr';
const KEY_USER_LANGUAGE = 'ul';
const KEY_USER_AGENT = 'ua';
const KEY_DOCUMENT_ENCODING = 'de';
const KEY_EVENT_CATEGORY = 'ec';
const KEY_EVENT_ACTION = 'ea';
const KEY_EVENT_LABEL = 'el';
const KEY_EVENT_VALUE = 'ev';
const KEY_ANONYMIZE_IP = 'aip';
const KEY_APPLICATION_NAME = 'an';
const KEY_APPLICATION_ID = 'aid';
const KEY_APPLICATION_VERSION = 'av';
const KEY_CUSTOM_DIMENSION_PREFIX = 'cd';
let _currentLocationPath = '/';
const segmentClient = new Analytics(getSegmentWriteKey(), {
// @ts-expect-error -- TSCONVERSION
axiosConfig: {
// This is needed to ensure that we use the NodeJS adapter in the render process
...(global?.require && {
adapter: global.require('axios/lib/adapters/http'),
}),
},
});
export function trackEvent(
category: string,
action: string,
label?: string | null,
value?: string | null,
) {
process.nextTick(async () => {
await _trackEvent({
interactive: true,
category,
action,
label,
value,
});
});
}
export function trackPageView(path: string) {
process.nextTick(async () => {
await _trackPageView(path);
});
}
export async function getDeviceId() {
const getDeviceId = async () => {
const settings = await models.settings.getOrCreate();
let { deviceId } = settings;
return settings.deviceId || (await models.settings.update(settings, { deviceId: uuid.v4() })).deviceId;
};
if (!deviceId) {
// Migrate old GA ID into settings model if needed
const oldId = (window && window.localStorage.getItem('gaClientId')) || null;
deviceId = oldId || uuid.v4();
await models.settings.update(settings, {
deviceId,
const sendSegment = async (segmentType: 'track' | 'page', options) => {
try {
const anonymousId = await getDeviceId();
const userId = getAccountId();
const context = {
app: { name: getAppName(), version: getAppVersion() },
os: { name: _getOsName(), version: process.getSystemVersion() },
};
segmentClient?.[segmentType]({ ...options, context, anonymousId, userId }, error => {
if (error) console.warn('[analytics] Error sending segment event', error);
});
} catch (error: unknown) {
console.warn('[analytics] Unexpected error while sending segment event', error);
}
return deviceId;
}
let segmentClient: Analytics | null = null;
};
export enum SegmentEvent {
appStarted = 'App Started',
collectionCreate = 'Collection Created',
criticalError = 'Critical Error Encountered',
dataExport = 'Data Exported',
dataImport = 'Data Imported',
documentCreate = 'Document Created',
kongConnected = 'Kong Connected',
kongSync = 'Kong Synced',
requestBodyTypeSelect = 'Request Body Type Selected',
requestCreate = 'Request Created',
requestExecute = 'Request Executed',
projectLocalCreate = 'Local Project Created',
@ -110,9 +73,9 @@ type PushPull = 'push' | 'pull';
export function vcsSegmentEventProperties(
type: 'git',
action: PushPull | `force_${PushPull}` |
'create_branch' | 'merge_branch' | 'delete_branch' | 'checkout_branch' |
'commit' | 'stage_all' | 'stage' | 'unstage_all' | 'unstage' | 'rollback' | 'rollback_all' |
'update' | 'setup' | 'clone',
'create_branch' | 'merge_branch' | 'delete_branch' | 'checkout_branch' |
'commit' | 'stage_all' | 'stage' | 'unstage_all' | 'unstage' | 'rollback' | 'rollback_all' |
'update' | 'setup' | 'clone',
error?: string
) {
return {
@ -138,7 +101,6 @@ interface QueuedSegmentEvent {
let queuedEvents: QueuedSegmentEvent[] = [];
async function flushQueuedEvents() {
console.log(`[segment] Flushing ${queuedEvents.length} queued events`, queuedEvents);
const events = [...queuedEvents];
// Clear queue before we even start sending to prevent races
@ -173,52 +135,21 @@ export async function trackSegmentEvent(
properties,
timestamp: new Date(),
};
console.log('[segment] Queued event', queuedEvent);
queuedEvents.push(queuedEvent);
}
return;
}
sendSegment('track', {
event,
properties,
...(timestamp ? { timestamp } : {}),
});
}
try {
if (!segmentClient) {
segmentClient = new Analytics(getSegmentWriteKey(), {
// @ts-expect-error -- TSCONVERSION
axiosConfig: {
// This is needed to ensure that we use the NodeJS adapter in the render process
...(global?.require && {
adapter: global.require('axios/lib/adapters/http'),
}),
},
});
}
const anonymousId = await getDeviceId();
// This may return an empty string or undefined when a user is not logged in
const userId = getAccountId();
segmentClient.track({
anonymousId,
userId,
event,
properties,
...(timestamp ? { timestamp } : {}),
context: {
app: {
name: getAppName(),
version: getAppVersion(),
},
os: {
name: _getOsName(),
version: process.getSystemVersion(),
},
},
}, error => {
if (error) {
console.warn('[analytics] Error sending segment event', error);
}
});
} catch (error: unknown) {
console.warn('[analytics] Unexpected error while sending segment event', error);
}
export async function trackPageView(name: string) {
const settings = await models.settings.getOrCreate();
if (!settings.enableAnalytics) return;
sendSegment('page', { name });
}
// ~~~~~~~~~~~~~~~~~ //
@ -226,156 +157,7 @@ export async function trackSegmentEvent(
// ~~~~~~~~~~~~~~~~~ //
function _getOsName() {
const platform = getAppPlatform();
switch (platform) {
case 'darwin':
return 'mac';
case 'win32':
return 'windows';
default:
return platform;
}
}
// Exported for testing
export async function _trackEvent({
interactive,
category,
action,
label,
value,
}: {
interactive: boolean;
category: string;
action: string;
label?: string | null;
value?: string | null;
}) {
const prefix = interactive ? '[ga] Event' : '[ga] Non-interactive';
console.log(prefix, [category, action, label, value].filter(Boolean).join(', '));
const params = [
{
name: KEY_HIT_TYPE,
value: 'event',
},
{
name: KEY_EVENT_CATEGORY,
value: category,
},
{
name: KEY_EVENT_ACTION,
value: action,
},
...(!interactive ? [{
name: KEY_NON_INTERACTION,
value: '1',
}] : []),
...(label ? [{
name: KEY_EVENT_LABEL,
value: label,
}] : []),
...(value ? [{
name: KEY_EVENT_VALUE,
value: value,
}] : []),
];
await _sendToGoogle({ params });
}
export async function _trackPageView(location: string) {
_currentLocationPath = location;
console.log('[ga] Page', _currentLocationPath);
const params = [
{
name: KEY_HIT_TYPE,
value: 'pageview',
},
];
await _sendToGoogle({ params });
}
async function _getDefaultParams(): Promise<RequestParameter[]> {
const deviceId = await getDeviceId();
// Prepping user agent string prior to sending to GA due to Electron base UA not being GA friendly.
const ua = String(window?.navigator?.userAgent)
.replace(new RegExp(`${getAppId()}\\/\\d+\\.\\d+\\.\\d+ `), '')
.replace(/Electron\/\d+\.\d+\.\d+ /, '');
const viewport = getViewportSize();
const params = [
{
name: KEY_VERSION,
value: '1',
},
{
name: KEY_TRACKING_ID,
value: getGoogleAnalyticsId(),
},
{
name: KEY_CLIENT_ID,
value: deviceId,
},
{
name: KEY_USER_AGENT,
value: ua,
},
{
name: KEY_LOCATION,
value: getGoogleAnalyticsLocation() + _currentLocationPath,
},
{
name: KEY_SCREEN_RESOLUTION,
value: getScreenResolution(),
},
{
name: KEY_USER_LANGUAGE,
value: getUserLanguage(),
},
{
name: KEY_TITLE,
value: `${getAppId()}:${getAppVersion()}`,
},
{
name: KEY_CUSTOM_DIMENSION_PREFIX + DIMENSION_PLATFORM,
value: getAppPlatform(),
},
{
name: KEY_CUSTOM_DIMENSION_PREFIX + DIMENSION_VERSION,
value: getAppVersion(),
},
{
name: KEY_ANONYMIZE_IP,
value: '1',
},
{
name: KEY_APPLICATION_NAME,
value: getAppName(),
},
{
name: KEY_APPLICATION_ID,
value: getAppId(),
},
{
name: KEY_APPLICATION_VERSION,
value: getAppVersion(),
},
...(viewport ? [{
name: KEY_VIEWPORT_SIZE,
value: viewport,
}] : []),
...(global.document ? [{
name: KEY_DOCUMENT_ENCODING,
value: global.document.inputEncoding,
}] : []),
];
return params;
return { darwin: 'mac', win32: 'windows' }[platform] || platform;
}
// Monitor database changes to see if analytics gets enabled.
@ -383,72 +165,9 @@ async function _getDefaultParams(): Promise<RequestParameter[]> {
db.onChange(async changes => {
for (const change of changes) {
const [event, doc] = change;
if (isSettings(doc) && event === 'update') {
if (doc.enableAnalytics) {
await flushQueuedEvents();
}
const isUpdatingSettings = isSettings(doc) && event === 'update';
if (isUpdatingSettings && doc.enableAnalytics) {
await flushQueuedEvents();
}
}
});
async function _sendToGoogle({ params }: { params: RequestParameter[] }) {
const settings = await models.settings.getOrCreate();
if (!settings.enableAnalytics) {
return;
}
const baseParams = await _getDefaultParams();
const allParams = [...baseParams, ...params];
const qs = buildQueryStringFromParams(allParams);
const baseUrl = isDevelopment()
? 'https://www.google-analytics.com/debug/collect'
: 'https://www.google-analytics.com/collect';
const url = joinUrlAndQueryString(baseUrl, qs);
const net = (electron.remote || electron).net;
const request = net.request(url);
request.once('error', err => {
console.warn('[ga] Network error', err);
});
request.once('response', response => {
const { statusCode } = response;
if (statusCode < 200 && statusCode >= 300) {
console.warn('[ga] Bad status code ' + statusCode);
}
const chunks: Buffer[] = [];
const [contentType] = response.headers['content-type'] || [];
if (contentType !== 'application/json') {
// Production GA API returns a Gif to use for tracking
return;
}
response.on('end', () => {
const jsonStr = Buffer.concat(chunks).toString('utf8');
try {
const data = JSON.parse(jsonStr);
const { hitParsingResult } = data;
if (hitParsingResult.valid) {
return;
}
for (const result of hitParsingResult || []) {
for (const msg of result.parserMessage || []) {
console.warn(`[ga] Error ${msg.description}`);
}
}
} catch (err) {
console.warn('[ga] Failed to parse response', err);
}
});
response.on('data', chunk => {
chunks.push(chunk);
});
});
request.end();
}

View File

@ -16,7 +16,7 @@ import { isUnitTest } from '../models/unit-test';
import { isUnitTestSuite } from '../models/unit-test-suite';
import { isWorkspace, Workspace } from '../models/workspace';
import { resetKeys } from '../sync/ignore-keys';
import { trackEvent } from './analytics';
import { SegmentEvent, trackSegmentEvent } from './analytics';
import {
EXPORT_TYPE_API_SPEC,
EXPORT_TYPE_COOKIE_JAR,
@ -113,7 +113,7 @@ export async function exportRequestsHAR(
}
const data = await har.exportHar(harRequests);
trackEvent('Data', 'Export', 'HAR');
trackSegmentEvent(SegmentEvent.dataExport);
return JSON.stringify(data, null, '\t');
}
@ -247,7 +247,7 @@ export async function exportRequestsData(
delete d.type;
return d;
});
trackEvent('Data', 'Export', `Insomnia ${format}`);
trackSegmentEvent(SegmentEvent.dataExport);
if (format.toLowerCase() === 'yaml') {
return YAML.stringify(data);

View File

@ -9,7 +9,7 @@ import { isWorkspace, Workspace } from '../models/workspace';
import { AlertModal } from '../ui/components/modals/alert-modal';
import { showError, showModal } from '../ui/components/modals/index';
import { ImportToWorkspacePrompt, SetWorkspaceScopePrompt } from '../ui/redux/modules/helpers';
import { trackEvent } from './analytics';
import { SegmentEvent, trackSegmentEvent } from './analytics';
import {
BASE_ENVIRONMENT_ID_KEY,
CONTENT_TYPE_GRAPHQL,
@ -321,7 +321,7 @@ export async function importRaw(
}
await db.flushChanges();
trackEvent('Data', 'Import', resultsType.id);
trackSegmentEvent(SegmentEvent.dataImport);
const importRequest: ImportResult = {
source: resultsType && typeof resultsType.id === 'string' ? resultsType.id : 'unknown',
summary: importedDocs,

View File

@ -9,7 +9,6 @@ import * as models from '../models';
import { getModelName } from '../models';
import type { Settings } from '../models/settings';
import { forceWorkspaceScopeToDesign } from '../sync/git/force-workspace-scope-to-design';
import { trackEvent } from './analytics';
import { database as db } from './database';
async function loadDesignerDb(
@ -158,7 +157,6 @@ export default async function migrateFromDesigner({
const modelTypesToMerge = [];
if (useDesignerSettings) {
trackEvent('Data', 'Migration', 'Settings');
// @ts-expect-error -- TSCONVERSION
modelTypesToMerge.push(models.settings.type);
console.log('[db-merge] keeping settings from Insomnia Designer');
@ -167,7 +165,6 @@ export default async function migrateFromDesigner({
}
if (copyWorkspaces) {
trackEvent('Data', 'Migration', 'Workspaces');
// @ts-expect-error -- TSCONVERSION
modelTypesToMerge.push(...workspaceModels);
}
@ -222,17 +219,14 @@ export default async function migrateFromDesigner({
if (copyPlugins) {
console.log('[db-merge] migrating plugins from designer to core');
trackEvent('Data', 'Migration', 'Plugins');
await migratePlugins(designerDataDir, coreDataDir);
}
console.log('[db-merge] done!');
trackEvent('Data', 'Migration', 'Success');
return {};
} catch (error) {
console.log('[db-merge] an error occurred while migrating');
console.error(error);
trackEvent('Data', 'Migration', 'Failure');
await restoreCoreBackup(backupDir, coreDataDir);
return {
error,

View File

@ -1,7 +1,6 @@
import * as git from 'isomorphic-git';
import path from 'path';
import { trackEvent } from '../../common/analytics';
import { httpClient } from './http-client';
import { convertToOsSep, convertToPosixSep } from './path-sep';
import { gitCallbacks } from './utils';
@ -223,7 +222,6 @@ export class GitVCS {
async commit(message: string) {
console.log(`[git] Commit "${message}"`);
trackEvent('Git', 'Commit');
return git.commit({ ...this._baseOpts, message });
}
@ -264,7 +262,6 @@ export class GitVCS {
async push(gitCredentials?: GitCredentials | null, force = false) {
console.log(`[git] Push remote=origin force=${force ? 'true' : 'false'}`);
trackEvent('Git', 'Push');
// eslint-disable-next-line no-unreachable
const response: git.PushResult = await git.push({
...this._baseOpts,
@ -286,7 +283,6 @@ export class GitVCS {
async pull(gitCredentials?: GitCredentials | null) {
console.log('[git] Pull remote=origin', await this.getBranch());
trackEvent('Git', 'Pull');
return git.pull({
...this._baseOpts,
...gitCallbacks(gitCredentials),
@ -298,7 +294,6 @@ export class GitVCS {
async merge(theirBranch: string) {
const ours = await this.getBranch();
console.log(`[git] Merge ${ours} <-- ${theirBranch}`);
trackEvent('Git', 'Merge');
return git.merge({
...this._baseOpts,
ours,
@ -336,13 +331,11 @@ export class GitVCS {
}
async branch(branch: string, checkout = false) {
trackEvent('Git', 'Create Branch');
// @ts-expect-error -- TSCONVERSION remote doesn't exist as an option
await git.branch({ ...this._baseOpts, ref: branch, checkout, remote: 'origin' });
}
async deleteBranch(branch: string) {
trackEvent('Git', 'Delete Branch');
await git.deleteBranch({ ...this._baseOpts, ref: branch });
}
@ -353,7 +346,6 @@ export class GitVCS {
const branches = await this.listBranches();
if (branches.includes(branch)) {
trackEvent('Git', 'Checkout Branch');
await git.checkout({ ...this._baseOpts, ref: branch, remote: 'origin' });
} else {
await this.branch(branch, true);

View File

@ -1,7 +1,7 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { PureComponent } from 'react';
import { trackEvent } from '../../../common/analytics';
import { SegmentEvent, trackSegmentEvent } from '../../../common/analytics';
import {
AUTOBIND_CFG,
CONTENT_TYPE_EDN,
@ -72,7 +72,7 @@ export class ContentTypeDropdown extends PureComponent<Props> {
}
this.props.onChange(mimeType);
trackEvent('Request', 'Change MimeType', mimeType);
trackSegmentEvent(SegmentEvent.requestBodyTypeSelect, { type:mimeType });
}
_renderDropdownItem(mimeType: string | null, forcedName = '') {

View File

@ -4,7 +4,7 @@ import React, { Fragment, PureComponent, ReactNode } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { SegmentEvent, trackEvent, trackSegmentEvent, vcsSegmentEventProperties } from '../../../common/analytics';
import { SegmentEvent, trackSegmentEvent, vcsSegmentEventProperties } from '../../../common/analytics';
import { AUTOBIND_CFG } from '../../../common/constants';
import { database as db } from '../../../common/database';
import { docsGitSync } from '../../../common/documentation';
@ -111,7 +111,7 @@ class GitSyncDropdown extends PureComponent<Props, State> {
}
async _handleOpen() {
trackEvent('Git Dropdown', 'Open');
trackSegmentEvent(SegmentEvent.vcsSyncStart, vcsSegmentEventProperties('git', 'setup'));
await this._refreshState();
}

View File

@ -1,4 +1,4 @@
import { trackEvent } from '../../../common/analytics';
import { trackPageView } from '../../../common/analytics';
import { AlertModal, AlertModalOptions } from './alert-modal';
import { ErrorModal, ErrorModalOptions } from './error-modal';
import { PromptModal, PromptModalOptions } from './prompt-modal';
@ -15,7 +15,7 @@ export function registerModal(instance) {
}
export function showModal(modalCls, ...args) {
trackEvent('Modals', 'Show', modalCls.name);
trackPageView(modalCls.name);
return _getModal(modalCls).show(...args);
}

View File

@ -1,7 +1,7 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { PureComponent } from 'react';
import { trackEvent } from '../../../common/analytics';
import { SegmentEvent, trackSegmentEvent } from '../../../common/analytics';
import {
AUTOBIND_CFG,
getContentTypeName,
@ -97,7 +97,7 @@ export class RequestCreateModal extends PureComponent<{}, State> {
}
this.hide();
trackEvent('Request', 'Create');
trackSegmentEvent(SegmentEvent.requestCreate);
}
_handleChangeSelectedContentType(selectedContentType: string | null) {

View File

@ -3,7 +3,6 @@ import { Tooltip } from 'insomnia-components';
import React, { FC, Fragment, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { trackEvent } from '../../../common/analytics';
import {
ACTIVITY_MIGRATION,
EditorKeyMap,
@ -102,7 +101,6 @@ export const General: FC<Props> = ({ hideModal }) => {
const settings = useSelector(selectSettings);
const handleStartMigration = useCallback(() => {
trackEvent('Data', 'Migration', 'Manual');
dispatch(setActiveActivity(ACTIVITY_MIGRATION));
hideModal();
}, [hideModal, dispatch]);

View File

@ -5,7 +5,6 @@ import styled from 'styled-components';
import YAML from 'yaml';
import YAMLSourceMap from 'yaml-source-map';
import { trackEvent } from '../../../common/analytics';
import { AUTOBIND_CFG } from '../../../common/constants';
import type { ApiSpec } from '../../../models/api-spec';
@ -41,7 +40,6 @@ export class SpecEditorSidebar extends Component<Props, State> {
col: number;
};
}) {
trackEvent('Spec Sidebar', 'Navigate');
const { handleSetSelection } = this.props;
// NOTE: We're subtracting 1 from everything because YAML CST uses
// 1-based indexing and we use 0-based.

View File

@ -3,7 +3,6 @@ import React, { FunctionComponent, useCallback, useMemo, useState } from 'react'
import { useDispatch } from 'react-redux';
import { useMount } from 'react-use';
import { trackEvent } from '../../common/analytics';
import { ACTIVITY_HOME } from '../../common/constants';
import { getDataDirectory, getDesignerDataDir, restartApp } from '../../common/electron-helpers';
import type { MigrationOptions } from '../../common/migrate-from-designer';
@ -276,7 +275,6 @@ const MigrationBody = () => {
const reduxDispatch = useDispatch();
const cancel = useCallback(() => {
trackEvent('Data', 'Migration', 'Skip');
reduxDispatch(setActiveActivity(ACTIVITY_HOME));
}, [reduxDispatch]);

View File

@ -2,7 +2,6 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator';
import * as importers from 'insomnia-importers';
import React, { Fragment, PureComponent, Ref } from 'react';
import { trackPageView } from '../../common/analytics';
import type { GlobalActivity } from '../../common/constants';
import {
ACTIVITY_DEBUG,
@ -439,21 +438,6 @@ export class Wrapper extends PureComponent<WrapperProps, State> {
});
}
componentDidMount() {
const { activity } = this.props;
trackPageView(`/${activity || ''}`);
}
componentDidUpdate(prevProps: WrapperProps) {
// We're using activities as page views so here we monitor
// for a change in activity and send it as a pageview.
const { activity } = this.props;
if (prevProps.activity !== activity) {
trackPageView(`/${activity || ''}`);
}
}
render() {
const {
activeCookieJar,

View File

@ -6,7 +6,7 @@ import { hot } from 'react-hot-loader';
import { Provider } from 'react-redux';
import * as styledComponents from 'styled-components';
import { trackEvent } from '../common/analytics';
import { SegmentEvent, trackSegmentEvent } from '../common/analytics';
import { getAppLongName, isDevelopment } from '../common/constants';
import { database as db } from '../common/database';
import { initializeLogging } from '../common/log';
@ -69,11 +69,11 @@ window['styled-components'] = styledComponents;
if (window && !isDevelopment()) {
window.addEventListener('error', e => {
console.error('Uncaught Error', e.error || e);
trackEvent('Error', 'Uncaught Error');
trackSegmentEvent(SegmentEvent.criticalError, { detail: e?.message });
});
window.addEventListener('unhandledrejection', e => {
console.error('Unhandled Promise', e.reason);
trackEvent('Error', 'Uncaught Promise');
trackSegmentEvent(SegmentEvent.criticalError, { detail: e?.reason });
});
}

View File

@ -8,7 +8,7 @@ import { mocked } from 'ts-jest/utils';
import { globalBeforeEach } from '../../../../__jest__/before-each';
import { reduxStateForTest } from '../../../../__jest__/redux-state-for-test';
import { SegmentEvent, trackEvent, trackSegmentEvent } from '../../../../common/analytics';
import { SegmentEvent, trackSegmentEvent } from '../../../../common/analytics';
import { ACTIVITY_SPEC } from '../../../../common/constants';
import * as models from '../../../../models';
import { gitRepositorySchema } from '../../../../models/__schemas__/model-schemas';
@ -84,7 +84,6 @@ describe('git', () => {
const shouldPromptToCreateWorkspace = async (memClient: PromiseFsClient) => {
const { repoSettings } = await dispatchCloneAndSubmitSettings(memClient);
expect(trackEvent).toHaveBeenCalledWith('Git', 'Clone');
// show alert asking to create a document
const alertArgs = getAndClearShowAlertMockArgs();
expect(alertArgs.title).toBe('No document found');
@ -111,7 +110,6 @@ describe('git', () => {
expect(meta?.gitRepositoryId).toBe(repoSettings._id);
// Ensure tracking events
expect(trackSegmentEvent).toHaveBeenCalledWith(SegmentEvent.documentCreate);
expect(trackEvent).toHaveBeenCalledWith('Workspace', 'Create');
// Ensure activity is activated
expect(store.getActions()).toEqual([
{

View File

@ -3,7 +3,6 @@ import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { globalBeforeEach } from '../../../../__jest__/before-each';
import { trackEvent } from '../../../../common/analytics';
import {
ACTIVITY_DEBUG,
ACTIVITY_HOME,
@ -68,7 +67,6 @@ describe('global', () => {
activity,
};
expect(setActiveActivity(activity)).toStrictEqual(expectedEvent);
expect(trackEvent).toHaveBeenCalledWith('Activity', 'Change', activity);
expect(global.localStorage.getItem(`${LOCALSTORAGE_PREFIX}::activity`)).toBe(
JSON.stringify(activity),
);

View File

@ -3,7 +3,7 @@ import thunk from 'redux-thunk';
import { globalBeforeEach } from '../../../../__jest__/before-each';
import { reduxStateForTest } from '../../../../__jest__/redux-state-for-test';
import { SegmentEvent, trackEvent, trackSegmentEvent } from '../../../../common/analytics';
import { SegmentEvent, trackSegmentEvent } from '../../../../common/analytics';
import { ACTIVITY_HOME } from '../../../../common/constants';
import * as models from '../../../../models';
import { DEFAULT_PROJECT_ID } from '../../../../models/project';
@ -49,7 +49,6 @@ describe('project', () => {
const project = projects[1];
expect(project.name).toBe(projectName);
expect(trackSegmentEvent).toHaveBeenCalledWith(SegmentEvent.projectLocalCreate);
expect(trackEvent).toHaveBeenCalledWith('Project', 'Create');
expect(store.getActions()).toEqual([
{
type: SET_ACTIVE_PROJECT,
@ -93,7 +92,6 @@ describe('project', () => {
const project = projects[1];
expect(project).toStrictEqual(projectTwo);
expect(trackSegmentEvent).toHaveBeenCalledWith(SegmentEvent.projectLocalDelete);
expect(trackEvent).toHaveBeenCalledWith('Project', 'Delete');
expect(store.getActions()).toEqual([
{
type: SET_ACTIVE_PROJECT,

View File

@ -3,7 +3,7 @@ import thunk from 'redux-thunk';
import { globalBeforeEach } from '../../../../__jest__/before-each';
import { reduxStateForTest } from '../../../../__jest__/redux-state-for-test';
import { SegmentEvent, trackEvent, trackSegmentEvent } from '../../../../common/analytics';
import { SegmentEvent, trackSegmentEvent } from '../../../../common/analytics';
import { ACTIVITY_DEBUG, ACTIVITY_SPEC, ACTIVITY_UNIT_TEST } from '../../../../common/constants';
import { database } from '../../../../common/database';
import * as models from '../../../../models';
@ -62,7 +62,6 @@ describe('workspace', () => {
const workspaceId = await expectedModelsCreated(workspaceName, WorkspaceScopeKeys.design, projectId);
expect(trackSegmentEvent).toHaveBeenCalledWith(SegmentEvent.documentCreate);
expect(trackEvent).toHaveBeenCalledWith('Workspace', 'Create');
expect(store.getActions()).toEqual([
{
type: SET_ACTIVE_PROJECT,
@ -96,7 +95,6 @@ describe('workspace', () => {
const workspaceId = await expectedModelsCreated(workspaceName, WorkspaceScopeKeys.collection, projectId);
expect(trackSegmentEvent).toHaveBeenCalledWith(SegmentEvent.collectionCreate);
expect(trackEvent).toHaveBeenCalledWith('Workspace', 'Create');
expect(store.getActions()).toEqual([
{
type: SET_ACTIVE_PROJECT,

View File

@ -3,7 +3,7 @@ import path from 'path';
import React, { ReactNode } from 'react';
import YAML from 'yaml';
import { SegmentEvent, trackEvent, trackSegmentEvent, vcsSegmentEventProperties } from '../../../common/analytics';
import { SegmentEvent, trackSegmentEvent, vcsSegmentEventProperties } from '../../../common/analytics';
import { database as db } from '../../../common/database';
import { strings } from '../../../common/strings';
import * as models from '../../../models';
@ -164,7 +164,6 @@ export const cloneGitRepository = ({ createFsClient }: {
dispatch(loadStart());
repoSettingsPatch.needsFullClone = true;
repoSettingsPatch.uri = translateSSHtoHTTP(repoSettingsPatch.uri);
trackEvent('Git', 'Clone');
let fsClient = createFsClient();
try {

View File

@ -6,7 +6,7 @@ import React, { Fragment } from 'react';
import { combineReducers, Dispatch } from 'redux';
import { unreachableCase } from 'ts-assert-unreachable';
import { trackEvent } from '../../../common/analytics';
import { trackPageView } from '../../../common/analytics';
import type { DashboardSortOrder, GlobalActivity } from '../../../common/constants';
import {
ACTIVITY_DEBUG,
@ -319,7 +319,7 @@ export const loadRequestStop = (requestId: string) => ({
export const setActiveActivity = (activity: GlobalActivity) => {
activity = _normalizeActivity(activity);
window.localStorage.setItem(`${LOCALSTORAGE_PREFIX}::activity`, JSON.stringify(activity));
trackEvent('Activity', 'Change', activity);
trackPageView(activity);
return {
type: SET_ACTIVE_ACTIVITY,
activity,
@ -690,7 +690,6 @@ export const initActiveActivity = () => (dispatch, getState) => {
} else {
// Always check if user has been prompted to migrate or onboard
if (!settings.hasPromptedToMigrateFromDesigner && fs.existsSync(getDesignerDataDir())) {
trackEvent('Data', 'Migration', 'Auto');
overrideActivity = ACTIVITY_MIGRATION;
}
}

View File

@ -1,4 +1,4 @@
import { SegmentEvent, trackEvent, trackSegmentEvent } from '../../../common/analytics';
import { SegmentEvent, trackSegmentEvent } from '../../../common/analytics';
import { ACTIVITY_HOME } from '../../../common/constants';
import { strings } from '../../../common/strings';
import * as models from '../../../models';
@ -18,7 +18,6 @@ export const createProject = () => dispatch => {
selectText: true,
onComplete: async name => {
const project = await models.project.create({ name });
trackEvent('Project', 'Create');
dispatch(setActiveProject(project._id));
dispatch(setActiveActivity(ACTIVITY_HOME));
trackSegmentEvent(SegmentEvent.projectLocalCreate);
@ -39,7 +38,6 @@ export const removeProject = (project: Project) => dispatch => {
onConfirm: async () => {
await models.stats.incrementDeletedRequestsForDescendents(project);
await models.project.remove(project);
trackEvent('Project', 'Delete');
// Show default project
dispatch(setActiveProject(DEFAULT_PROJECT_ID));
// Show home in case not already on home

View File

@ -1,7 +1,7 @@
import { Dispatch } from 'redux';
import { RequireExactlyOne } from 'type-fest';
import { SegmentEvent, trackEvent, trackSegmentEvent } from '../../../common/analytics';
import { SegmentEvent, trackSegmentEvent } from '../../../common/analytics';
import { ACTIVITY_DEBUG, ACTIVITY_SPEC, GlobalActivity, isCollectionActivity, isDesignActivity } from '../../../common/constants';
import { database } from '../../../common/database';
import * as models from '../../../models';
@ -31,7 +31,8 @@ const actuallyCreate = (patch: Partial<Workspace>, onCreate?: OnWorkspaceCreateC
await onCreate(workspace);
}
trackEvent('Workspace', 'Create');
trackSegmentEvent(SegmentEvent.collectionCreate);
await dispatch(activateWorkspace({ workspace }));
};
};

View File

@ -2382,6 +2382,14 @@
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"dev": true
},
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
},
"babel-jest": {
"version": "26.6.3",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz",
@ -4299,6 +4307,11 @@
"readable-stream": "^2.3.6"
}
},
"follow-redirects": {
"version": "1.14.7",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ=="
},
"for-in": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",

View File

@ -69,6 +69,7 @@
},
"dependencies": {
"@stoplight/spectral": "^5.9.0",
"axios": "^0.21.2",
"commander": "^5.1.0",
"consola": "^2.15.0",
"cosmiconfig": "^6.0.0",

View File

@ -13,7 +13,7 @@ export const documentActions = [
spec={spec}
store={context.store}
axios={context.__private.axios}
trackEvent={context.__private.analytics.trackEvent}
trackSegmentEvent={context.__private.analytics.trackSegmentEvent}
/>,
root,
);

View File

@ -12,7 +12,7 @@ type Props = {
data: Object,
status: number,
}>,
trackEvent: (category: string, action: string, label: ?string, value: ?string) => any,
trackSegmentEvent: (event: string, properties?: Record<string, any>) => any,
store: {
hasItem: (key: string) => Promise<boolean>,
setItem: (key: string, value: string) => Promise<void>,
@ -90,7 +90,7 @@ class DeployToPortal extends React.Component<Props, State> {
e.preventDefault();
}
const { spec, axios, trackEvent } = this.props;
const { spec, axios, trackSegmentEvent } = this.props;
const {
kongSpecFileName,
@ -145,12 +145,14 @@ class DeployToPortal extends React.Component<Props, State> {
});
if (response.statusText === 'Created' || response.statusText === 'OK') {
this.setState({ kongPortalDeployView: 'success' });
trackEvent('Portal', 'Upload', overwrite ? 'Replace' : 'Create');
const action = overwrite ? 'replace_portal' : 'create_portal'
trackSegmentEvent('Kong Synced', { type: 'deploy', action })
}
} catch (err) {
if (err.response && err.response.status === 409) {
this.setState({ kongPortalDeployView: 'overwrite' });
trackEvent('Portal', 'Upload Error', overwrite ? 'Replace' : 'Create');
const action = overwrite ? 'replace_portal' : 'create_portal'
trackSegmentEvent('Kong Synced', { type: 'deploy', action, error: err.response.status + ': ' + err.response.statusText })
} else {
console.log('Failed to upload to dev portal', err.response);
if (err.response && err.response.data && err.response.data.message) {
@ -164,7 +166,7 @@ class DeployToPortal extends React.Component<Props, State> {
async _handleConnectKong(e: SyntheticEvent<HTMLFormElement>) {
e.preventDefault();
const { axios, trackEvent } = this.props;
const { axios, trackSegmentEvent } = this.props;
const { kongPortalUserWorkspace, kongPortalApiUrl, kongPortalRbacToken } = this.state;
@ -182,7 +184,8 @@ class DeployToPortal extends React.Component<Props, State> {
},
});
if (response.status === 200 || response.status === 201) {
trackEvent('Portal', 'Connection');
trackSegmentEvent('Kong Connected', { type: 'token', action: 'portal_deploy' })
// Set legacy mode for post upload formatting, suppress loader, set monitor portal URL, move to upload view
const guiHost = response.data.configuration.portal_gui_host;
this.setState({
@ -195,7 +198,8 @@ class DeployToPortal extends React.Component<Props, State> {
this._handleLoadingToggle(false);
}
} catch (error) {
trackEvent('Portal', 'Connection Error');
trackSegmentEvent('Kong Connected', { type: 'token', action: 'portal_deploy', error: error.message })
console.log('Connection error', error);
this._handleLoadingToggle(false);
this.setState({ connectionError: error });