(core) Add telemetry

Test Plan: Server tests.

Reviewers: jarek

Differential Revision: https://phab.getgrist.com/D3818
This commit is contained in:
George Gevoian
2023-04-06 11:10:29 -04:00
parent 6a4b7d96e8
commit a19ba0813a
28 changed files with 555 additions and 44 deletions

View File

@@ -15,6 +15,7 @@
// tslint:disable:unified-signatures
import {logTelemetryEvent} from 'app/client/lib/telemetry';
import {AppModel} from 'app/client/models/AppModel';
import {reportWarning} from 'app/client/models/errors';
import {IAppError} from 'app/client/models/NotifyModel';
@@ -54,6 +55,11 @@ interface ISessionData {
[key: string]: string;
}
interface ICallbackAttributes {
id?: string;
query?: string;
}
/**
* This provides the HelpScout Beacon API, taking care of initializing Beacon on first use.
*/
@@ -65,7 +71,8 @@ export function Beacon(method: 'navigate', route: string): void;
export function Beacon(method: 'identify', userObj: IUserObj): void;
export function Beacon(method: 'prefill', formObj: IFormObj): void;
export function Beacon(method: 'config', configObj: object): void;
export function Beacon(method: 'on'|'once', event: string, callback: () => void): void;
export function Beacon(method: 'on'|'once', event: string,
callback: (attrs?: ICallbackAttributes) => void): void;
export function Beacon(method: 'off', event: string, callback?: () => void): void;
export function Beacon(method: 'session-data', data: ISessionData): void;
export function Beacon(method: BeaconCmd): void;
@@ -187,6 +194,15 @@ function _beaconOpen(userObj: IUserObj|null, options: IBeaconOpenOptions) {
if (!skipNav) {
Beacon('navigate', route);
}
Beacon('once', 'open', () => logTelemetryEvent('beaconOpen'));
Beacon('on', 'article-viewed', (article) => logTelemetryEvent('beaconArticleViewed', {
articleId: article!.id,
}));
Beacon('on', 'email-sent', () => logTelemetryEvent('beaconEmailSent'));
Beacon('on', 'search', (search) => logTelemetryEvent('beaconSearch', {
searchQuery: search!.query,
}));
}
function fixBeaconBaseHref() {

View File

@@ -0,0 +1,23 @@
import {logError} from 'app/client/models/errors';
import {TelemetryEventName} from 'app/common/Telemetry';
import {fetchFromHome, pageHasHome} from 'app/common/urlUtils';
export async function logTelemetryEvent(name: TelemetryEventName, metadata?: Record<string, any>) {
if (!pageHasHome()) { return; }
await fetchFromHome('/api/telemetry', {
method: 'POST',
body: JSON.stringify({
name,
metadata,
}),
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
}).catch((e: Error) => {
console.warn(`Failed to log telemetry event ${name}`, e);
logError(e);
});
}

View File

@@ -59,7 +59,7 @@ export function reportMessage(msg: MessageType, options?: Partial<INotifyOptions
export function reportWarning(msg: string, options?: Partial<INotifyOptions>) {
options = {level: 'warning', ...options};
log.warn(`${options.level}: `, msg);
_logError(msg);
logError(msg);
return reportMessage(msg, options);
}
@@ -84,7 +84,7 @@ export function reportError(err: Error|string): void {
// This error can be emitted while a page is reloaded, and isn't worth reporting.
return;
}
_logError(err);
logError(err);
if (_notifier && !_notifier.isDisposed()) {
if (!isError(err)) {
err = new Error(String(err));
@@ -175,7 +175,7 @@ export function setUpErrorHandling(doReportError = reportError, koUtil?: any) {
* over-logging (regular errors such as access rights or account limits) and
* under-logging (javascript errors during startup might never get reported).
*/
function _logError(error: Error|string) {
export function logError(error: Error|string) {
if (!pageHasHome()) { return; }
const docId = G.window.gristDocPageModel?.currentDocId?.get();
fetchFromHome('/api/log', {

View File

@@ -1,5 +1,6 @@
import * as commands from 'app/client/components/commands';
import {makeT} from 'app/client/lib/localization';
import {logTelemetryEvent} from 'app/client/lib/telemetry';
import {getMainOrgUrl} from 'app/client/models/gristUrlState';
import {cssLinkText, cssPageEntryMain, cssPageIcon, cssPageLink} from 'app/client/ui/LeftPanelCommon';
import {YouTubePlayer} from 'app/client/ui/YouTubePlayer';
@@ -20,7 +21,26 @@ const VIDEO_TOUR_YOUTUBE_EMBED_ID = 'qnr2Pfnxdlc';
*/
export function openVideoTour(refElement: HTMLElement) {
return modal(
(ctl) => {
(ctl, owner) => {
const youtubePlayer = YouTubePlayer.create(owner,
VIDEO_TOUR_YOUTUBE_EMBED_ID,
{
onPlayerReady: (player) => player.playVideo(),
height: '100%',
width: '100%',
origin: getMainOrgUrl(),
},
cssYouTubePlayer.cls(''),
);
owner.onDispose(async () => {
if (youtubePlayer.isLoading()) { return; }
await logTelemetryEvent('watchedVideoTour', {
watchTimeSeconds: Math.floor(youtubePlayer.getCurrentTime()),
});
});
return [
cssModal.cls(''),
cssModalCloseButton(
@@ -28,18 +48,7 @@ const VIDEO_TOUR_YOUTUBE_EMBED_ID = 'qnr2Pfnxdlc';
dom.on('click', () => ctl.close()),
testId('close'),
),
cssYouTubePlayerContainer(
dom.create(YouTubePlayer,
VIDEO_TOUR_YOUTUBE_EMBED_ID,
{
onPlayerReady: (player) => player.playVideo(),
height: '100%',
width: '100%',
origin: getMainOrgUrl(),
},
cssYouTubePlayer.cls(''),
),
),
cssYouTubePlayerContainer(youtubePlayer.buildDom()),
testId('modal'),
];
},

View File

@@ -10,6 +10,7 @@ export interface Player {
mute(): void;
unMute(): void;
setVolume(volume: number): void;
getCurrentTime(): number;
}
export interface PlayerOptions {
@@ -80,6 +81,10 @@ export class YouTubePlayer extends Disposable {
}
}
public isLoading() {
return this._isLoading();
}
public isLoaded() {
return waitObs(this._isLoading, (val) => !val);
}
@@ -92,6 +97,10 @@ export class YouTubePlayer extends Disposable {
this._player.setVolume(volume);
}
public getCurrentTime(): number {
return this._player.getCurrentTime();
}
public buildDom() {
return dom('div', {id: this._playerId}, ...this._domArgs);
}