mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Add telemetry
Test Plan: Server tests. Reviewers: jarek Differential Revision: https://phab.getgrist.com/D3818
This commit is contained in:
@@ -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() {
|
||||
|
||||
23
app/client/lib/telemetry.ts
Normal file
23
app/client/lib/telemetry.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -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', {
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user