mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +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