mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(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:
@@ -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.
|
||||
|
||||
@@ -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};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user