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: {
|
||||
description: 'Triggered when a document is forked.',
|
||||
minimumTelemetryLevel: Level.limited,
|
||||
@ -898,6 +937,7 @@ export const TelemetryEvents = StringUnion(
|
||||
'beaconArticleViewed',
|
||||
'beaconEmailSent',
|
||||
'beaconSearch',
|
||||
'documentCreated',
|
||||
'documentForked',
|
||||
'documentOpened',
|
||||
'documentUsage',
|
||||
|
@ -13,6 +13,7 @@ import {getAuthorizedUserId, getUserId, getUserProfiles, RequestWithLogin} from
|
||||
import {getSessionUser, linkOrgWithEmail} from 'app/server/lib/BrowserSession';
|
||||
import {expressWrap} from 'app/server/lib/expressWrap';
|
||||
import {RequestWithOrg} from 'app/server/lib/extractOrg';
|
||||
import {GristServer} from 'app/server/lib/GristServer';
|
||||
import {getTemplateOrg} from 'app/server/lib/gristSettings';
|
||||
import log from 'app/server/lib/log';
|
||||
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.
|
||||
*/
|
||||
constructor(
|
||||
private _gristServer: GristServer,
|
||||
private _app: express.Application,
|
||||
private _dbManager: HomeDBManager,
|
||||
private _widgetRepository: IWidgetRepository
|
||||
@ -235,8 +237,23 @@ export class ApiServer {
|
||||
// POST /api/workspaces/:wid/docs
|
||||
// Create a new doc owned by the specific workspace.
|
||||
this._app.post('/api/workspaces/:wid/docs', expressWrap(async (req, res) => {
|
||||
const mreq = req as RequestWithLogin;
|
||||
const wsId = integerParam(req.params.wid, 'wid');
|
||||
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);
|
||||
}));
|
||||
|
||||
|
@ -17,6 +17,7 @@ import {SchemaTypes} from "app/common/schema";
|
||||
import {SortFunc} from 'app/common/SortFunc';
|
||||
import {Sort} from 'app/common/SortSpec';
|
||||
import {MetaRowRecord} from 'app/common/TableData';
|
||||
import {TelemetryMetadataByLevel} from "app/common/Telemetry";
|
||||
import {WebhookFields} from "app/common/Triggers";
|
||||
import TriggersTI from 'app/common/Triggers-ti';
|
||||
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
|
||||
// actual file uploads, so no worries here about large request bodies.)
|
||||
this._app.post('/api/workspaces/:wid/import', expressWrap(async (req, res) => {
|
||||
const mreq = req as RequestWithLogin;
|
||||
const userId = getUserId(req);
|
||||
const wsId = integerParam(req.params.wid, 'wid');
|
||||
const uploadId = integerParam(req.body.uploadId, 'uploadId');
|
||||
@ -1158,6 +1160,16 @@ export class DocWorkerApi {
|
||||
uploadId,
|
||||
workspaceId: wsId,
|
||||
browserSettings: req.body.browserSettings,
|
||||
telemetryMetadata: {
|
||||
limited: {
|
||||
isImport: true,
|
||||
sourceDocIdDigest: undefined,
|
||||
},
|
||||
full: {
|
||||
userId: mreq.userId,
|
||||
altSessionId: mreq.altSessionId,
|
||||
},
|
||||
},
|
||||
});
|
||||
res.json(result);
|
||||
}));
|
||||
@ -1243,6 +1255,7 @@ export class DocWorkerApi {
|
||||
* TODO: unify this with the other document creation and import endpoints.
|
||||
*/
|
||||
this._app.post('/api/docs', checkAnonymousCreation, expressWrap(async (req, res) => {
|
||||
const mreq = req as RequestWithLogin;
|
||||
const userId = getUserId(req);
|
||||
|
||||
let uploadId: number|undefined;
|
||||
@ -1279,6 +1292,16 @@ export class DocWorkerApi {
|
||||
documentName: optStringParam(parameters.documentName, 'documentName'),
|
||||
workspaceId,
|
||||
browserSettings,
|
||||
telemetryMetadata: {
|
||||
limited: {
|
||||
isImport: true,
|
||||
sourceDocIdDigest: undefined,
|
||||
},
|
||||
full: {
|
||||
userId: mreq.userId,
|
||||
altSessionId: mreq.altSessionId,
|
||||
},
|
||||
},
|
||||
});
|
||||
docId = result.id;
|
||||
} else if (workspaceId !== undefined) {
|
||||
@ -1304,6 +1327,7 @@ export class DocWorkerApi {
|
||||
documentName: string,
|
||||
asTemplate?: boolean,
|
||||
}): Promise<string> {
|
||||
const mreq = req as RequestWithLogin;
|
||||
const {userId, sourceDocumentId, workspaceId, documentName, asTemplate = false} = options;
|
||||
|
||||
// First, upload a copy of the document.
|
||||
@ -1326,6 +1350,16 @@ export class DocWorkerApi {
|
||||
uploadId: uploadResult.uploadId,
|
||||
documentName,
|
||||
workspaceId,
|
||||
telemetryMetadata: {
|
||||
limited: {
|
||||
isImport: false,
|
||||
sourceDocIdDigest: sourceDocumentId,
|
||||
},
|
||||
full: {
|
||||
userId: mreq.userId,
|
||||
altSessionId: mreq.altSessionId,
|
||||
},
|
||||
},
|
||||
});
|
||||
return result.id;
|
||||
}
|
||||
@ -1341,7 +1375,15 @@ export class DocWorkerApi {
|
||||
if (status !== 200) {
|
||||
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!;
|
||||
}
|
||||
|
||||
@ -1365,9 +1407,29 @@ export class DocWorkerApi {
|
||||
}),
|
||||
docId
|
||||
);
|
||||
this._logDocumentCreatedTelemetryEvent(req, {
|
||||
limited: {
|
||||
docIdDigest: docId,
|
||||
sourceDocIdDigest: undefined,
|
||||
isImport: false,
|
||||
fileType: undefined,
|
||||
isSaved: false,
|
||||
},
|
||||
});
|
||||
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
|
||||
* 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 {Invite} from 'app/common/sharing';
|
||||
import {tbind} from 'app/common/tbind';
|
||||
import {TelemetryMetadataByLevel} from 'app/common/Telemetry';
|
||||
import {NEW_DOCUMENT_CODE} from 'app/common/UserAPI';
|
||||
import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
|
||||
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 {PluginManager} from './PluginManager';
|
||||
import {getFileUploadInfo, globalUploadSet, makeAccessId, UploadInfo} from './uploads';
|
||||
import merge = require('lodash/merge');
|
||||
import noop = require('lodash/noop');
|
||||
|
||||
// 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,
|
||||
workspaceId?: number,
|
||||
browserSettings?: BrowserSettings,
|
||||
telemetryMetadata?: TelemetryMetadataByLevel,
|
||||
}): Promise<DocCreationInfo> {
|
||||
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 docSession = makeExceptionalDocSession('nascent', {browserSettings});
|
||||
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);
|
||||
}
|
||||
};
|
||||
return this._doImportDoc(docSession,
|
||||
globalUploadSet.getUploadInfo(uploadId, accessId), {
|
||||
naming: workspaceId ? 'saved' : 'unsaved',
|
||||
register,
|
||||
userId,
|
||||
});
|
||||
const uploadInfo = globalUploadSet.getUploadInfo(uploadId, accessId);
|
||||
const docCreationInfo = await this._doImportDoc(docSession, uploadInfo, {
|
||||
naming: workspaceId ? 'saved' : 'unsaved',
|
||||
register,
|
||||
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.
|
||||
// We could break that association (see /api/docs/:docId/assign for how) if
|
||||
// we start using dedicated import workers.
|
||||
|
@ -670,7 +670,7 @@ export class FlexServer implements GristServer {
|
||||
|
||||
// ApiServer's constructor adds endpoints to the app.
|
||||
// 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() {
|
||||
|
Loading…
Reference in New Issue
Block a user