mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Add documentCreated telemetry event
Summary: The event is triggered whenever a document is created, imported, or duplicated. Test Plan: Tested manually. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D4035
This commit is contained in:
parent
76e822eb23
commit
40c5f7b738
@ -353,6 +353,45 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
documentCreated: {
|
||||||
|
description: 'Triggered when a document is created.',
|
||||||
|
minimumTelemetryLevel: Level.limited,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the id of the created document.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
sourceDocIdDigest: {
|
||||||
|
description: 'A hash of the id of the source document, if the document was '
|
||||||
|
+ 'duplicated from an existing document.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
isImport: {
|
||||||
|
description: 'Whether the document was created by import.',
|
||||||
|
dataType: 'boolean',
|
||||||
|
},
|
||||||
|
isSaved: {
|
||||||
|
description: 'Whether the document was saved to a workspace.',
|
||||||
|
dataType: 'boolean',
|
||||||
|
},
|
||||||
|
fileType: {
|
||||||
|
description: 'If the document was created by import, the file extension '
|
||||||
|
+ 'of the file that was imported.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
documentForked: {
|
documentForked: {
|
||||||
description: 'Triggered when a document is forked.',
|
description: 'Triggered when a document is forked.',
|
||||||
minimumTelemetryLevel: Level.limited,
|
minimumTelemetryLevel: Level.limited,
|
||||||
@ -898,6 +937,7 @@ export const TelemetryEvents = StringUnion(
|
|||||||
'beaconArticleViewed',
|
'beaconArticleViewed',
|
||||||
'beaconEmailSent',
|
'beaconEmailSent',
|
||||||
'beaconSearch',
|
'beaconSearch',
|
||||||
|
'documentCreated',
|
||||||
'documentForked',
|
'documentForked',
|
||||||
'documentOpened',
|
'documentOpened',
|
||||||
'documentUsage',
|
'documentUsage',
|
||||||
|
@ -13,6 +13,7 @@ import {getAuthorizedUserId, getUserId, getUserProfiles, RequestWithLogin} from
|
|||||||
import {getSessionUser, linkOrgWithEmail} from 'app/server/lib/BrowserSession';
|
import {getSessionUser, linkOrgWithEmail} from 'app/server/lib/BrowserSession';
|
||||||
import {expressWrap} from 'app/server/lib/expressWrap';
|
import {expressWrap} from 'app/server/lib/expressWrap';
|
||||||
import {RequestWithOrg} from 'app/server/lib/extractOrg';
|
import {RequestWithOrg} from 'app/server/lib/extractOrg';
|
||||||
|
import {GristServer} from 'app/server/lib/GristServer';
|
||||||
import {getTemplateOrg} from 'app/server/lib/gristSettings';
|
import {getTemplateOrg} from 'app/server/lib/gristSettings';
|
||||||
import log from 'app/server/lib/log';
|
import log from 'app/server/lib/log';
|
||||||
import {addPermit, clearSessionCacheIfNeeded, getDocScope, getScope, integerParam,
|
import {addPermit, clearSessionCacheIfNeeded, getDocScope, getScope, integerParam,
|
||||||
@ -100,6 +101,7 @@ export class ApiServer {
|
|||||||
* to apply to these routes, and trustOrigin too for cross-domain requests.
|
* to apply to these routes, and trustOrigin too for cross-domain requests.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
|
private _gristServer: GristServer,
|
||||||
private _app: express.Application,
|
private _app: express.Application,
|
||||||
private _dbManager: HomeDBManager,
|
private _dbManager: HomeDBManager,
|
||||||
private _widgetRepository: IWidgetRepository
|
private _widgetRepository: IWidgetRepository
|
||||||
@ -235,8 +237,23 @@ export class ApiServer {
|
|||||||
// POST /api/workspaces/:wid/docs
|
// POST /api/workspaces/:wid/docs
|
||||||
// Create a new doc owned by the specific workspace.
|
// Create a new doc owned by the specific workspace.
|
||||||
this._app.post('/api/workspaces/:wid/docs', expressWrap(async (req, res) => {
|
this._app.post('/api/workspaces/:wid/docs', expressWrap(async (req, res) => {
|
||||||
|
const mreq = req as RequestWithLogin;
|
||||||
const wsId = integerParam(req.params.wid, 'wid');
|
const wsId = integerParam(req.params.wid, 'wid');
|
||||||
const query = await this._dbManager.addDocument(getScope(req), wsId, req.body);
|
const query = await this._dbManager.addDocument(getScope(req), wsId, req.body);
|
||||||
|
this._gristServer.getTelemetry().logEvent('documentCreated', {
|
||||||
|
limited: {
|
||||||
|
docIdDigest: query.data!,
|
||||||
|
sourceDocIdDigest: undefined,
|
||||||
|
isImport: false,
|
||||||
|
fileType: undefined,
|
||||||
|
isSaved: true,
|
||||||
|
},
|
||||||
|
full: {
|
||||||
|
userId: mreq.userId,
|
||||||
|
altSessionId: mreq.altSessionId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(e => log.error('failed to log telemetry event documentCreated', e));
|
||||||
return sendReply(req, res, query);
|
return sendReply(req, res, query);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import {SchemaTypes} from "app/common/schema";
|
|||||||
import {SortFunc} from 'app/common/SortFunc';
|
import {SortFunc} from 'app/common/SortFunc';
|
||||||
import {Sort} from 'app/common/SortSpec';
|
import {Sort} from 'app/common/SortSpec';
|
||||||
import {MetaRowRecord} from 'app/common/TableData';
|
import {MetaRowRecord} from 'app/common/TableData';
|
||||||
|
import {TelemetryMetadataByLevel} from "app/common/Telemetry";
|
||||||
import {WebhookFields} from "app/common/Triggers";
|
import {WebhookFields} from "app/common/Triggers";
|
||||||
import TriggersTI from 'app/common/Triggers-ti';
|
import TriggersTI from 'app/common/Triggers-ti';
|
||||||
import {DocReplacementOptions, DocState, DocStateComparison, DocStates, NEW_DOCUMENT_CODE} from 'app/common/UserAPI';
|
import {DocReplacementOptions, DocState, DocStateComparison, DocStates, NEW_DOCUMENT_CODE} from 'app/common/UserAPI';
|
||||||
@ -1150,6 +1151,7 @@ export class DocWorkerApi {
|
|||||||
// endpoint is handled only by DocWorker, so is handled here. (Note: this does not handle
|
// endpoint is handled only by DocWorker, so is handled here. (Note: this does not handle
|
||||||
// actual file uploads, so no worries here about large request bodies.)
|
// actual file uploads, so no worries here about large request bodies.)
|
||||||
this._app.post('/api/workspaces/:wid/import', expressWrap(async (req, res) => {
|
this._app.post('/api/workspaces/:wid/import', expressWrap(async (req, res) => {
|
||||||
|
const mreq = req as RequestWithLogin;
|
||||||
const userId = getUserId(req);
|
const userId = getUserId(req);
|
||||||
const wsId = integerParam(req.params.wid, 'wid');
|
const wsId = integerParam(req.params.wid, 'wid');
|
||||||
const uploadId = integerParam(req.body.uploadId, 'uploadId');
|
const uploadId = integerParam(req.body.uploadId, 'uploadId');
|
||||||
@ -1158,6 +1160,16 @@ export class DocWorkerApi {
|
|||||||
uploadId,
|
uploadId,
|
||||||
workspaceId: wsId,
|
workspaceId: wsId,
|
||||||
browserSettings: req.body.browserSettings,
|
browserSettings: req.body.browserSettings,
|
||||||
|
telemetryMetadata: {
|
||||||
|
limited: {
|
||||||
|
isImport: true,
|
||||||
|
sourceDocIdDigest: undefined,
|
||||||
|
},
|
||||||
|
full: {
|
||||||
|
userId: mreq.userId,
|
||||||
|
altSessionId: mreq.altSessionId,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
res.json(result);
|
res.json(result);
|
||||||
}));
|
}));
|
||||||
@ -1243,6 +1255,7 @@ export class DocWorkerApi {
|
|||||||
* TODO: unify this with the other document creation and import endpoints.
|
* TODO: unify this with the other document creation and import endpoints.
|
||||||
*/
|
*/
|
||||||
this._app.post('/api/docs', checkAnonymousCreation, expressWrap(async (req, res) => {
|
this._app.post('/api/docs', checkAnonymousCreation, expressWrap(async (req, res) => {
|
||||||
|
const mreq = req as RequestWithLogin;
|
||||||
const userId = getUserId(req);
|
const userId = getUserId(req);
|
||||||
|
|
||||||
let uploadId: number|undefined;
|
let uploadId: number|undefined;
|
||||||
@ -1279,6 +1292,16 @@ export class DocWorkerApi {
|
|||||||
documentName: optStringParam(parameters.documentName, 'documentName'),
|
documentName: optStringParam(parameters.documentName, 'documentName'),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
browserSettings,
|
browserSettings,
|
||||||
|
telemetryMetadata: {
|
||||||
|
limited: {
|
||||||
|
isImport: true,
|
||||||
|
sourceDocIdDigest: undefined,
|
||||||
|
},
|
||||||
|
full: {
|
||||||
|
userId: mreq.userId,
|
||||||
|
altSessionId: mreq.altSessionId,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
docId = result.id;
|
docId = result.id;
|
||||||
} else if (workspaceId !== undefined) {
|
} else if (workspaceId !== undefined) {
|
||||||
@ -1304,6 +1327,7 @@ export class DocWorkerApi {
|
|||||||
documentName: string,
|
documentName: string,
|
||||||
asTemplate?: boolean,
|
asTemplate?: boolean,
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
|
const mreq = req as RequestWithLogin;
|
||||||
const {userId, sourceDocumentId, workspaceId, documentName, asTemplate = false} = options;
|
const {userId, sourceDocumentId, workspaceId, documentName, asTemplate = false} = options;
|
||||||
|
|
||||||
// First, upload a copy of the document.
|
// First, upload a copy of the document.
|
||||||
@ -1326,6 +1350,16 @@ export class DocWorkerApi {
|
|||||||
uploadId: uploadResult.uploadId,
|
uploadId: uploadResult.uploadId,
|
||||||
documentName,
|
documentName,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
telemetryMetadata: {
|
||||||
|
limited: {
|
||||||
|
isImport: false,
|
||||||
|
sourceDocIdDigest: sourceDocumentId,
|
||||||
|
},
|
||||||
|
full: {
|
||||||
|
userId: mreq.userId,
|
||||||
|
altSessionId: mreq.altSessionId,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return result.id;
|
return result.id;
|
||||||
}
|
}
|
||||||
@ -1341,7 +1375,15 @@ export class DocWorkerApi {
|
|||||||
if (status !== 200) {
|
if (status !== 200) {
|
||||||
throw new ApiError(errMessage || 'unable to create document', status);
|
throw new ApiError(errMessage || 'unable to create document', status);
|
||||||
}
|
}
|
||||||
|
this._logDocumentCreatedTelemetryEvent(req, {
|
||||||
|
limited: {
|
||||||
|
docIdDigest: data!,
|
||||||
|
sourceDocIdDigest: undefined,
|
||||||
|
isImport: false,
|
||||||
|
fileType: undefined,
|
||||||
|
isSaved: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
return data!;
|
return data!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1365,9 +1407,29 @@ export class DocWorkerApi {
|
|||||||
}),
|
}),
|
||||||
docId
|
docId
|
||||||
);
|
);
|
||||||
|
this._logDocumentCreatedTelemetryEvent(req, {
|
||||||
|
limited: {
|
||||||
|
docIdDigest: docId,
|
||||||
|
sourceDocIdDigest: undefined,
|
||||||
|
isImport: false,
|
||||||
|
fileType: undefined,
|
||||||
|
isSaved: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
return docId;
|
return docId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _logDocumentCreatedTelemetryEvent(req: Request, metadata: TelemetryMetadataByLevel) {
|
||||||
|
const mreq = req as RequestWithLogin;
|
||||||
|
this._grist.getTelemetry().logEvent('documentCreated', _.merge({
|
||||||
|
full: {
|
||||||
|
userId: mreq.userId,
|
||||||
|
altSessionId: mreq.altSessionId,
|
||||||
|
},
|
||||||
|
}, metadata))
|
||||||
|
.catch(e => log.error('failed to log telemetry event documentCreated', e));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for read access to the given document, and return its
|
* Check for read access to the given document, and return its
|
||||||
* canonical docId. Throws error if read access not available.
|
* canonical docId. Throws error if read access not available.
|
||||||
|
@ -12,6 +12,7 @@ import {DocCreationInfo, DocEntry, DocListAPI, OpenDocMode, OpenLocalDocResult}
|
|||||||
import {FilteredDocUsageSummary} from 'app/common/DocUsage';
|
import {FilteredDocUsageSummary} from 'app/common/DocUsage';
|
||||||
import {Invite} from 'app/common/sharing';
|
import {Invite} from 'app/common/sharing';
|
||||||
import {tbind} from 'app/common/tbind';
|
import {tbind} from 'app/common/tbind';
|
||||||
|
import {TelemetryMetadataByLevel} from 'app/common/Telemetry';
|
||||||
import {NEW_DOCUMENT_CODE} from 'app/common/UserAPI';
|
import {NEW_DOCUMENT_CODE} from 'app/common/UserAPI';
|
||||||
import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
|
import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
|
||||||
import {assertAccess, Authorizer, DocAuthorizer, DummyAuthorizer, isSingleUserMode} from 'app/server/lib/Authorizer';
|
import {assertAccess, Authorizer, DocAuthorizer, DummyAuthorizer, isSingleUserMode} from 'app/server/lib/Authorizer';
|
||||||
@ -31,6 +32,7 @@ import log from 'app/server/lib/log';
|
|||||||
import {ActiveDoc} from './ActiveDoc';
|
import {ActiveDoc} from './ActiveDoc';
|
||||||
import {PluginManager} from './PluginManager';
|
import {PluginManager} from './PluginManager';
|
||||||
import {getFileUploadInfo, globalUploadSet, makeAccessId, UploadInfo} from './uploads';
|
import {getFileUploadInfo, globalUploadSet, makeAccessId, UploadInfo} from './uploads';
|
||||||
|
import merge = require('lodash/merge');
|
||||||
import noop = require('lodash/noop');
|
import noop = require('lodash/noop');
|
||||||
|
|
||||||
// A TTL in milliseconds to use for material that can easily be recomputed / refetched
|
// A TTL in milliseconds to use for material that can easily be recomputed / refetched
|
||||||
@ -202,10 +204,11 @@ export class DocManager extends EventEmitter {
|
|||||||
documentName?: string,
|
documentName?: string,
|
||||||
workspaceId?: number,
|
workspaceId?: number,
|
||||||
browserSettings?: BrowserSettings,
|
browserSettings?: BrowserSettings,
|
||||||
|
telemetryMetadata?: TelemetryMetadataByLevel,
|
||||||
}): Promise<DocCreationInfo> {
|
}): Promise<DocCreationInfo> {
|
||||||
if (!this._homeDbManager) { throw new Error("HomeDbManager not available"); }
|
if (!this._homeDbManager) { throw new Error("HomeDbManager not available"); }
|
||||||
|
|
||||||
const {userId, uploadId, documentName, workspaceId, browserSettings} = options;
|
const {userId, uploadId, documentName, workspaceId, browserSettings, telemetryMetadata} = options;
|
||||||
const accessId = this.makeAccessId(userId);
|
const accessId = this.makeAccessId(userId);
|
||||||
const docSession = makeExceptionalDocSession('nascent', {browserSettings});
|
const docSession = makeExceptionalDocSession('nascent', {browserSettings});
|
||||||
const register = async (docId: string, uploadBaseFilename: string) => {
|
const register = async (docId: string, uploadBaseFilename: string) => {
|
||||||
@ -222,13 +225,23 @@ export class DocManager extends EventEmitter {
|
|||||||
throw new ApiError(queryResult.errMessage || 'unable to add imported document', queryResult.status);
|
throw new ApiError(queryResult.errMessage || 'unable to add imported document', queryResult.status);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return this._doImportDoc(docSession,
|
const uploadInfo = globalUploadSet.getUploadInfo(uploadId, accessId);
|
||||||
globalUploadSet.getUploadInfo(uploadId, accessId), {
|
const docCreationInfo = await this._doImportDoc(docSession, uploadInfo, {
|
||||||
naming: workspaceId ? 'saved' : 'unsaved',
|
naming: workspaceId ? 'saved' : 'unsaved',
|
||||||
register,
|
register,
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.gristServer.getTelemetry().logEvent('documentCreated', merge({
|
||||||
|
limited: {
|
||||||
|
docIdDigest: docCreationInfo.id,
|
||||||
|
fileType: uploadInfo.files[0].ext.trim().slice(1),
|
||||||
|
isSaved: workspaceId !== undefined,
|
||||||
|
},
|
||||||
|
}, telemetryMetadata))
|
||||||
|
.catch(e => log.error('failed to log telemetry event documentCreated', e));
|
||||||
|
|
||||||
|
return docCreationInfo;
|
||||||
// The imported document is associated with the worker that did the import.
|
// The imported document is associated with the worker that did the import.
|
||||||
// We could break that association (see /api/docs/:docId/assign for how) if
|
// We could break that association (see /api/docs/:docId/assign for how) if
|
||||||
// we start using dedicated import workers.
|
// we start using dedicated import workers.
|
||||||
|
@ -670,7 +670,7 @@ export class FlexServer implements GristServer {
|
|||||||
|
|
||||||
// ApiServer's constructor adds endpoints to the app.
|
// ApiServer's constructor adds endpoints to the app.
|
||||||
// tslint:disable-next-line:no-unused-expression
|
// tslint:disable-next-line:no-unused-expression
|
||||||
new ApiServer(this.app, this._dbManager, this._widgetRepository = buildWidgetRepository());
|
new ApiServer(this, this.app, this._dbManager, this._widgetRepository = buildWidgetRepository());
|
||||||
}
|
}
|
||||||
|
|
||||||
public addBillingApi() {
|
public addBillingApi() {
|
||||||
|
Loading…
Reference in New Issue
Block a user