Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: telemetry api #4968

Merged
merged 27 commits into from
Dec 3, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2dcce27
feat: telemetry api
tdurnford Nov 24, 2020
b3be74c
Merge branch 'main' into feature/telemetry
tdurnford Nov 24, 2020
d1074d8
lint
tdurnford Nov 24, 2020
8ba8d4c
persist machine id
tdurnford Nov 25, 2020
ab3fc90
Merge branch 'main' into feature/telemetry
tdurnford Nov 25, 2020
83eae7e
Merge branch 'main' into feature/telemetry
tdurnford Nov 29, 2020
a2dee47
move telemetry settings to user settings
tdurnford Nov 30, 2020
1973702
Merge branch 'main' into feature/telemetry
tdurnford Nov 30, 2020
eca7b6c
reverted changes to en-US
tdurnford Nov 30, 2020
e8c6998
pool telemetry events
tdurnford Nov 30, 2020
0d92815
Merge branch 'main' into feature/telemetry
tdurnford Nov 30, 2020
17e4956
changed batch size
tdurnford Nov 30, 2020
360b52f
Merge branch 'main' into feature/telemetry
tdurnford Nov 30, 2020
6bbc52c
Merge branch 'main' into feature/telemetry
tdurnford Dec 1, 2020
df7f772
Merge branch 'main' into feature/telemetry
tdurnford Dec 2, 2020
392293e
fix uuid
tdurnford Dec 2, 2020
7becbd9
Merge branch 'main' into feature/telemetry
tdurnford Dec 2, 2020
e5cc57c
add telemetry classes
tdurnford Dec 3, 2020
97127bb
Merge branch 'main' into feature/telemetry
tdurnford Dec 3, 2020
0f36e5f
Merge branch 'main' into feature/telemetry
tdurnford Dec 3, 2020
3a31333
requested changes
tdurnford Dec 3, 2020
9b68368
Merge branch 'main' into feature/telemetry
tdurnford Dec 3, 2020
d6ab6ef
shorted interval
tdurnford Dec 3, 2020
0591985
Merge branch 'main' into feature/telemetry
a-b-r-o-w-n Dec 3, 2020
c5f0858
fix integration test
tdurnford Dec 3, 2020
1377c3a
change parameter type
tdurnford Dec 3, 2020
0b84f0a
Merge branch 'main' into feature/telemetry
tdurnford Dec 3, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('<CreationFlow/>', () => {
setCreationFlowStatus: jest.fn(),
navTo: jest.fn(),
saveTemplateId: jest.fn(),
setCurrentPageMode: jest.fn(),
});
set(creationFlowStatusState, CreationFlowStatus.NEW_FROM_TEMPLATE);
set(featureFlagsState, getDefaultFeatureFlags());
Expand Down
3 changes: 3 additions & 0 deletions Composer/packages/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { MainContainer } from './components/AppComponents/MainContainer';
import { userSettingsState } from './recoilModel';
import { loadLocale } from './utils/fileUtil';
import { dispatcherState } from './recoilModel/DispatcherWrapper';
import { useInitializeLogger } from './telemetry/hooks';

initializeIcons(undefined, { disableWarnings: true });

Expand All @@ -28,6 +29,8 @@ export const App: React.FC = () => {
fetchServerSettings();
}, []);

useInitializeLogger();

return (
<Fragment key={appLocale}>
<Announcement />
Expand Down
16 changes: 12 additions & 4 deletions Composer/packages/client/src/pages/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

/** @jsx jsx */
import { jsx } from '@emotion/core';
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import formatMessage from 'format-message';
import { Link } from 'office-ui-fabric-react/lib/Link';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
Expand Down Expand Up @@ -60,11 +60,19 @@ const Home: React.FC<RouteComponentProps> = () => {
const botName = useRecoilValue(botDisplayNameState(projectId));
const recentProjects = useRecoilValue(recentProjectsState);
const templateId = useRecoilValue(templateIdState);
const { openProject, setCreationFlowStatus, onboardingAddCoachMarkRef, saveTemplateId } = useRecoilValue(
dispatcherState
);
const {
openProject,
setCreationFlowStatus,
onboardingAddCoachMarkRef,
saveTemplateId,
setCurrentPageMode,
} = useRecoilValue(dispatcherState);
const filteredTemplates = useRecoilValue(filteredTemplatesSelector);

useEffect(() => {
setCurrentPageMode('home');
}, []);

const onItemChosen = async (item) => {
if (item && item.path) {
openProject(item.path);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { TelemetryEventTypes } from '@bfc/shared';

import httpClient from '../../utils/httpUtil';
import { getApplicationInsightsLogger } from '../applicationInsightsLogger';

jest.mock('../../utils/httpUtil');

describe('Application Insights Logger', () => {
it('should log event to the server', async () => {
(httpClient.post as jest.Mock).mockResolvedValue({});

const eventLogger = getApplicationInsightsLogger();
eventLogger.logEvent('TestEvent', { value: '1' });
eventLogger.flush();

expect(httpClient.post).toBeCalledWith(
'/telemetry/event',
expect.objectContaining({
type: TelemetryEventTypes.TrackEvent,
name: 'TestEvent',
properties: {
value: '1',
},
})
);
});

it('should log page views to the server', async () => {
(httpClient.post as jest.Mock).mockResolvedValue({});

const eventLogger = getApplicationInsightsLogger();
eventLogger.logPageView('TestEvent', 'https://composer', { value: '1' });
eventLogger.flush();

expect(httpClient.post).toBeCalledWith(
'/telemetry/event',
expect.objectContaining({
type: TelemetryEventTypes.PageView,
name: 'TestEvent',
url: 'https://composer',
properties: {
value: '1',
},
})
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { addPropsToLogger } from '../telemetryLogger';

describe('addPropsToLogger', () => {
it('adds property to logEvent', () => {
const mockLogger = {
logEvent: jest.fn(),
logPageView: jest.fn(),
flush: jest.fn(),
};

const logger = addPropsToLogger(mockLogger, { prop1: 'prop1' });
logger.logEvent('Test', { prop0: 'prop0' });
expect(mockLogger.logEvent).toBeCalledWith(
'Test',
expect.objectContaining({
prop0: 'prop0',
prop1: 'prop1',
})
);
});

it('adds property to logPageView', () => {
const mockLogger = {
logEvent: jest.fn(),
logPageView: jest.fn(),
flush: jest.fn(),
};

const logger = addPropsToLogger(mockLogger, { prop1: 'prop1' });
logger.logPageView('Test', 'https://composer', { prop0: 'prop0' });
expect(mockLogger.logPageView).toBeCalledWith(
'Test',
'https://composer',
expect.objectContaining({
prop0: 'prop0',
prop1: 'prop1',
})
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { LogData, TelemetryLogger, TelemetryEventTypes } from '@bfc/shared';

import httpClient from '../utils/httpUtil';

type Event = {
type: TelemetryEventTypes;
name: string;
url?: string;
properties?: LogData;
};

export const appInsightsLogger = (): TelemetryLogger => {
const events: Event[] = [];

const shift = async () => {
const event = events.shift();
if (event) {
try {
await httpClient.post('/telemetry/event', event);
} catch (error) {
// Swallow error to avoid crashing the app while sending telemetry
}
}
};

setInterval(() => {
shift();
}, 5000);

const logEvent = (name: string, properties?: LogData) => {
events.push({ type: TelemetryEventTypes.TrackEvent, name, properties });
};

const logPageView = (name: string, url: string, properties?: LogData) => {
events.push({ type: TelemetryEventTypes.PageView, name, url, properties });
};

const flush = () => {
while (events.length) {
shift();
}
};

return {
logEvent,
logPageView,
flush,
};
};

const logger = {
current: appInsightsLogger(),
};

export const getApplicationInsightsLogger = () => {
return logger.current;
};
25 changes: 25 additions & 0 deletions Composer/packages/client/src/telemetry/consoleLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { LogData, TelemetryLogger } from '@bfc/shared';
import noop from 'lodash/noop';

export const consoleLogger = (): TelemetryLogger => {
const logEvent = (name: string, properties?: LogData) => {
console.log('bfc-telemetry:', { name, properties });
};

const logPageView = (name: string, url: string, properties?: LogData) => {
console.log('bfc-telemetry:', { name, url, properties });
};

return {
logEvent,
logPageView,
flush: noop,
};
};

export const getConsoleLogger = () => {
return consoleLogger();
};
33 changes: 33 additions & 0 deletions Composer/packages/client/src/telemetry/getEventLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import pickBy from 'lodash/pickBy';
import { TelemetryEventLogger, LogData, TelemetryEventName, TelemetryEvents } from '@botframework-composer/types';

import { addPropsToLogger, getLogger } from './telemetryLogger';

export const getEventLogger = (properties?: LogData | (() => LogData)): TelemetryEventLogger => {
const logger = addPropsToLogger(getLogger(), properties);

const log = <TN extends TelemetryEventName>(
eventName: TN,
...args: TelemetryEvents[TN] extends undefined ? [never?] : [TelemetryEvents[TN]]
) => {
const [properties] = args;
logger.logEvent(eventName, pickBy(properties as any));
};

const pageView = <TN extends TelemetryEventName>(
eventName: TN,
url: string,
...args: TelemetryEvents[TN] extends undefined ? [never?] : [TelemetryEvents[TN]]
) => {
const [properties] = args;
logger.logPageView(eventName, url, pickBy(properties as any));
};

return {
log,
pageView,
};
};
34 changes: 34 additions & 0 deletions Composer/packages/client/src/telemetry/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { LogData } from '@bfc/shared';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';

import { currentModeState, currentProjectIdState, ServerSettingsState } from '../recoilModel';

import { getEventLogger } from './getEventLogger';
import { createLogger, initializeLogger } from './telemetryLogger';

export const useInitializeLogger = () => {
const [, forceRender] = useState({});
const projectId = useRecoilValue(currentProjectIdState);
const page = useRecoilValue(currentModeState);
const { telemetry } = useRecoilValue(ServerSettingsState);

useEffect(() => {
initializeLogger(createLogger(telemetry), () => ({
timestamp: new Date().toUTCString(),
composerVersion: process.env.COMPOSER_VERSION || 'unknown',
sdkPackageVersion: process.env.SDK_PACKAGE_VERSION || 'unknown',
page,
projectId,
}));

forceRender({});
}, [telemetry, page, projectId]);
};

export const useEventLogger = (properties?: LogData | (() => LogData)) => {
return getEventLogger(properties);
};
58 changes: 58 additions & 0 deletions Composer/packages/client/src/telemetry/telemetryLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { LogData, TelemetryLogger, TelemetrySettings } from '@botframework-composer/types';
import noop from 'lodash/noop';

import { getApplicationInsightsLogger } from './applicationInsightsLogger';
import { getConsoleLogger } from './consoleLogger';
import { getEventLogger } from './getEventLogger';

export const getNoopLogger = (): TelemetryLogger => {
return {
logEvent: noop,
logPageView: noop,
flush: noop,
};
};

// Event Logger Singleton
const theLogger = {
current: getNoopLogger(),
};

export const createLogger = (telemetrySettings?: TelemetrySettings): TelemetryLogger => {
return telemetrySettings?.allowDataCollection
? process.env.NODE_ENV !== 'development'
? getApplicationInsightsLogger()
: getConsoleLogger()
: getNoopLogger();
};

export const addPropsToLogger = (
logger: TelemetryLogger,
addProperties?: LogData | (() => LogData)
): TelemetryLogger => {
const getProperties = typeof addProperties === 'function' ? addProperties : () => addProperties;

const logEvent = (name: string, properties?: LogData) => {
logger.logEvent(name, { ...getProperties(), ...properties });
};

const logPageView = (name: string, url: string, properties?: LogData) => {
logger.logPageView(name, url, { ...getProperties(), ...properties });
};

return {
...logger,
logPageView,
logEvent,
};
};

export const initializeLogger = (logger: TelemetryLogger, properties?: LogData | (() => LogData)) => {
theLogger.current = addPropsToLogger(logger, properties);
return getEventLogger();
};

export const getLogger = () => theLogger.current;
Loading