(core) Add more telemetry events

Summary: Adds new telemetry events and a flag for whether an event originated from a team site.

Test Plan: Manual.

Reviewers: paulfitz

Reviewed By: paulfitz

Subscribers: jarek, dsagal

Differential Revision: https://phab.getgrist.com/D4105
This commit is contained in:
George Gevoian
2023-11-15 15:20:51 -05:00
parent a14543008d
commit c9bba5207e
6 changed files with 439 additions and 18 deletions

View File

@@ -1185,6 +1185,11 @@ export class DocWorkerApi {
},
},
});
this._logCreatedFileImportDocTelemetryEvent(req, {
full: {
docIdDigest: result.id,
},
});
res.json(result);
}));
@@ -1319,6 +1324,11 @@ export class DocWorkerApi {
},
});
docId = result.id;
this._logCreatedFileImportDocTelemetryEvent(req, {
full: {
docIdDigest: docId,
},
});
} else if (workspaceId !== undefined) {
docId = await this._createNewSavedDoc(req, {
workspaceId: workspaceId,
@@ -1376,6 +1386,30 @@ export class DocWorkerApi {
},
},
});
const sourceDocument = await this._dbManager.getRawDocById(sourceDocumentId);
const isTemplateCopy = sourceDocument.type === 'template';
if (isTemplateCopy) {
this._grist.getTelemetry().logEvent(mreq, 'copiedTemplate', {
full: {
templateId: parseUrlId(sourceDocument.urlId || sourceDocument.id).trunkId,
userId: mreq.userId,
altSessionId: mreq.altSessionId,
},
});
}
this._grist.getTelemetry().logEvent(
mreq,
`createdDoc-${isTemplateCopy ? 'CopyTemplate' : 'CopyDoc'}`,
{
full: {
docIdDigest: result.id,
userId: mreq.userId,
altSessionId: mreq.altSessionId,
},
}
);
return result.id;
}
@@ -1387,19 +1421,25 @@ export class DocWorkerApi {
const {status, data, errMessage} = await this._dbManager.addDocument(getScope(req), workspaceId, {
name: documentName ?? 'Untitled document',
});
const docId = data!;
if (status !== 200) {
throw new ApiError(errMessage || 'unable to create document', status);
}
this._logDocumentCreatedTelemetryEvent(req, {
limited: {
docIdDigest: data!,
docIdDigest: docId,
sourceDocIdDigest: undefined,
isImport: false,
fileType: undefined,
isSaved: true,
},
});
return data!;
this._logCreatedEmptyDocTelemetryEvent(req, {
full: {
docIdDigest: docId,
},
});
return docId;
}
private async _createNewUnsavedDoc(req: Request, options: {
@@ -1431,6 +1471,11 @@ export class DocWorkerApi {
isSaved: false,
},
});
this._logCreatedEmptyDocTelemetryEvent(req, {
full: {
docIdDigest: docId,
},
});
return docId;
}
@@ -1444,6 +1489,28 @@ export class DocWorkerApi {
}, metadata));
}
private _logCreatedEmptyDocTelemetryEvent(req: Request, metadata: TelemetryMetadataByLevel) {
this._logCreatedDocTelemetryEvent(req, 'createdDoc-Empty', metadata);
}
private _logCreatedFileImportDocTelemetryEvent(req: Request, metadata: TelemetryMetadataByLevel) {
this._logCreatedDocTelemetryEvent(req, 'createdDoc-FileImport', metadata);
}
private _logCreatedDocTelemetryEvent(
req: Request,
event: 'createdDoc-Empty' | 'createdDoc-FileImport',
metadata: TelemetryMetadataByLevel,
) {
const mreq = req as RequestWithLogin;
this._grist.getTelemetry().logEvent(mreq, event, _.merge({
full: {
userId: mreq.userId,
altSessionId: mreq.altSessionId,
},
}, metadata));
}
/**
* Check for read access to the given document, and return its
* canonical docId. Throws error if read access not available.

View File

@@ -301,13 +301,14 @@ export class DocManager extends EventEmitter {
openMode: OpenDocMode = 'default',
linkParameters: Record<string, string> = {}): Promise<OpenLocalDocResult> {
let auth: Authorizer;
let userId: number | undefined;
const dbManager = this._homeDbManager;
if (!isSingleUserMode()) {
if (!dbManager) { throw new Error("HomeDbManager not available"); }
// Sets up authorization of the document.
const org = client.getOrg();
if (!org) { throw new Error('Documents can only be opened in the context of a specific organization'); }
const userId = await client.getUserId(dbManager) || dbManager.getAnonymousUserId();
userId = await client.getUserId(dbManager) || dbManager.getAnonymousUserId();
const userRef = await client.getUserRef(dbManager);
// We use docId in the key, and disallow urlId, so we can be sure that we are looking at the
@@ -397,6 +398,14 @@ export class DocManager extends EventEmitter {
this.emit('open-doc', this.storageManager.getPath(activeDoc.docName));
}
this.gristServer.getTelemetry().logEvent(docSession, 'openedDoc', {
full: {
docIdDigest: docId,
userId,
altSessionId: client.getAltSessionId(),
},
});
return {activeDoc, result};
});
}

View File

@@ -252,13 +252,23 @@ export class Telemetry implements ITelemetry {
metadata?: TelemetryMetadata
) {
let isInternalUser: boolean | undefined;
let isTeamSite: boolean | undefined;
if (requestOrSession) {
const email = ('get' in requestOrSession)
? requestOrSession.user?.loginEmail
: getDocSessionUser(requestOrSession)?.email;
let email: string | undefined;
let org: string | undefined;
if ('get' in requestOrSession) {
email = requestOrSession.user?.loginEmail;
org = requestOrSession.org;
} else {
email = getDocSessionUser(requestOrSession)?.email;
org = requestOrSession.client?.getOrg() ?? requestOrSession.req?.org;
}
if (email) {
isInternalUser = email !== 'anon@getgrist.com' && email.endsWith('@getgrist.com');
}
if (org && !process.env.GRIST_SINGLE_ORG) {
isTeamSite = !this._dbManager.isMergedOrg(org);
}
}
const {category: eventCategory} = TelemetryContracts[event];
this._telemetryLogger.rawLog('info', getEventType(event), event, {
@@ -268,6 +278,7 @@ export class Telemetry implements ITelemetry {
eventSource: `grist-${this._deploymentType}`,
installationId: this._activation!.id,
...(isInternalUser !== undefined ? {isInternalUser} : undefined),
...(isTeamSite !== undefined ? {isTeamSite} : undefined),
});
}

View File

@@ -116,13 +116,13 @@ export function makeSendAppPage(opts: {
}) {
const {server, staticDir, tag, testLogin} = opts;
return async (req: express.Request, resp: express.Response, options: ISendAppPageOptions) => {
const config = makeGristConfig({
homeUrl: !isSingleUserMode() ? server.getHomeUrl(req) : null,
extra: options.config,
baseDomain: opts.baseDomain,
req,
server,
});
const config = makeGristConfig({
homeUrl: !isSingleUserMode() ? server.getHomeUrl(req) : null,
extra: options.config,
baseDomain: opts.baseDomain,
req,
server,
});
// We could cache file contents in memory, but the filesystem does caching too, and compared
// to that, the performance gain is unlikely to be meaningful. So keep it simple here.
@@ -156,10 +156,46 @@ export function makeSendAppPage(opts: {
"<!-- INSERT CONFIG -->",
`<script>window.gristConfig = ${jsesc(config, {isScriptContext: true, json: true})};</script>`
);
logVisitedPageTelemetryEvent(req as RequestWithLogin, {
server,
pagePath: options.path,
docId: config.assignmentId,
});
resp.status(options.status).type('html').send(content);
};
}
interface LogVisitedPageEventOptions {
server: GristServer;
pagePath: string;
docId?: string;
}
function logVisitedPageTelemetryEvent(req: RequestWithLogin, options: LogVisitedPageEventOptions) {
const {server, pagePath, docId} = options;
// Construct a fake URL and append the utm_* parameters from the original URL.
// We avoid using the original URL here because it may contain sensitive identifiers,
// such as link key parameters and site/doc ids.
const url = new URL('fake', server.getMergedOrgUrl(req));
for (const [key, value] of Object.entries(req.query)) {
if (key.startsWith('utm_')) {
url.searchParams.set(key, String(value));
}
}
server.getTelemetry().logEvent(req, 'visitedPage', {
full: {
docIdDigest: docId,
url: url.toString(),
path: pagePath,
userAgent: req.headers['user-agent'],
userId: req.userId,
altSessionId: req.altSessionId,
},
});
}
function shouldSupportAnon() {
// Enable UI for anonymous access if a flag is explicitly set in the environment
return process.env.GRIST_SUPPORT_ANON === "true";