mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) Polish telemetry code
Summary: Also fixes a few small bugs with telemetry collection. Test Plan: Server and manual tests. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3915
This commit is contained in:
		
							parent
							
								
									4629068c25
								
							
						
					
					
						commit
						a460563daf
					
				
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -130,12 +130,12 @@ export class Document extends Resource {
 | 
			
		||||
            this.options.tutorial = null;
 | 
			
		||||
          } else {
 | 
			
		||||
            this.options.tutorial = this.options.tutorial || {};
 | 
			
		||||
            if (props.options.tutorial.lastSlideIndex !== undefined) {
 | 
			
		||||
              this.options.tutorial.lastSlideIndex = props.options.tutorial.lastSlideIndex;
 | 
			
		||||
            }
 | 
			
		||||
            if (props.options.tutorial.numSlides !== undefined) {
 | 
			
		||||
              this.options.tutorial.numSlides = props.options.tutorial.numSlides;
 | 
			
		||||
              if (dbManager && props.options?.tutorial?.lastSlideIndex !== undefined) {
 | 
			
		||||
            }
 | 
			
		||||
            if (props.options.tutorial.lastSlideIndex !== undefined) {
 | 
			
		||||
              this.options.tutorial.lastSlideIndex = props.options.tutorial.lastSlideIndex;
 | 
			
		||||
              if (dbManager && this.options.tutorial.numSlides) {
 | 
			
		||||
                this._emitTutorialProgressChangeEvent(dbManager, this.options.tutorial);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -906,6 +906,7 @@ export class DocWorkerApi {
 | 
			
		||||
                name: tutorialTrunk.name,
 | 
			
		||||
                options: {
 | 
			
		||||
                  tutorial: {
 | 
			
		||||
                    ...tutorialTrunk.options?.tutorial,
 | 
			
		||||
                    // For now, the only state we need to reset is the slide position.
 | 
			
		||||
                    lastSlideIndex: 0,
 | 
			
		||||
                  },
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,8 @@ export interface ITelemetry {
 | 
			
		||||
  getTelemetryLevel(): TelemetryLevel;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const MAX_PENDING_FORWARD_EVENT_REQUESTS = 25;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Manages telemetry for Grist.
 | 
			
		||||
 */
 | 
			
		||||
@ -34,10 +36,11 @@ export class Telemetry implements ITelemetry {
 | 
			
		||||
  private _shouldForwardTelemetryEvents = this._deploymentType !== 'saas';
 | 
			
		||||
  private _forwardTelemetryEventsUrl = process.env.GRIST_TELEMETRY_URL ||
 | 
			
		||||
  'https://telemetry.getgrist.com/api/telemetry';
 | 
			
		||||
  private _numPendingForwardEventRequests = 0;
 | 
			
		||||
 | 
			
		||||
  private _installationId: string | undefined;
 | 
			
		||||
 | 
			
		||||
  private _errorLogger = new LogMethods('Telemetry ', () => ({}));
 | 
			
		||||
  private _logger = new LogMethods('Telemetry ', () => ({}));
 | 
			
		||||
  private _telemetryLogger = new LogMethods('Telemetry ', () => ({
 | 
			
		||||
    eventType: 'telemetry',
 | 
			
		||||
  }));
 | 
			
		||||
@ -46,7 +49,7 @@ export class Telemetry implements ITelemetry {
 | 
			
		||||
 | 
			
		||||
  constructor(private _dbManager: HomeDBManager, private _gristServer: GristServer) {
 | 
			
		||||
    this._initialize().catch((e) => {
 | 
			
		||||
      this._errorLogger.error(undefined, 'failed to initialize', e);
 | 
			
		||||
      this._logger.error(undefined, 'failed to initialize', e);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -99,7 +102,7 @@ export class Telemetry implements ITelemetry {
 | 
			
		||||
    this._checkTelemetryEvent(event, metadata);
 | 
			
		||||
 | 
			
		||||
    if (this._shouldForwardTelemetryEvents) {
 | 
			
		||||
      await this.forwardEvent(event, metadata);
 | 
			
		||||
      await this._forwardEvent(event, metadata);
 | 
			
		||||
    } else {
 | 
			
		||||
      this._telemetryLogger.rawLog('info', null, event, {
 | 
			
		||||
        eventName: event,
 | 
			
		||||
@ -109,29 +112,6 @@ export class Telemetry implements ITelemetry {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Forwards a telemetry event and its metadata to another server.
 | 
			
		||||
   */
 | 
			
		||||
  public async forwardEvent(
 | 
			
		||||
    event: TelemetryEvent,
 | 
			
		||||
    metadata?: TelemetryMetadata
 | 
			
		||||
  ) {
 | 
			
		||||
    try {
 | 
			
		||||
      await fetch(this._forwardTelemetryEventsUrl, {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json',
 | 
			
		||||
        },
 | 
			
		||||
        body: JSON.stringify({
 | 
			
		||||
          event,
 | 
			
		||||
          metadata,
 | 
			
		||||
        }),
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      this._errorLogger.error(undefined, `failed to forward telemetry event ${event}`, e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public addEndpoints(app: express.Application) {
 | 
			
		||||
    /**
 | 
			
		||||
     * Logs telemetry events and their metadata.
 | 
			
		||||
@ -169,7 +149,7 @@ export class Telemetry implements ITelemetry {
 | 
			
		||||
            req.body.metadata,
 | 
			
		||||
          ));
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          this._errorLogger.error(undefined, `failed to log telemetry event ${event}`, e);
 | 
			
		||||
          this._logger.error(undefined, `failed to log telemetry event ${event}`, e);
 | 
			
		||||
          throw new ApiError(`Telemetry failed to log telemetry event ${event}`, 500);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@ -195,7 +175,7 @@ export class Telemetry implements ITelemetry {
 | 
			
		||||
    for (const event of HomeDBTelemetryEvents.values) {
 | 
			
		||||
      this._dbManager.on(event, async (metadata) => {
 | 
			
		||||
        this.logEvent(event, metadata).catch(e =>
 | 
			
		||||
          this._errorLogger.error(undefined, `failed to log telemetry event ${event}`, e));
 | 
			
		||||
          this._logger.error(undefined, `failed to log telemetry event ${event}`, e));
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -207,4 +187,34 @@ export class Telemetry implements ITelemetry {
 | 
			
		||||
 | 
			
		||||
    this._checkEvent(event, metadata);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _forwardEvent(
 | 
			
		||||
    event: TelemetryEvent,
 | 
			
		||||
    metadata?: TelemetryMetadata
 | 
			
		||||
  ) {
 | 
			
		||||
    if (this._numPendingForwardEventRequests === MAX_PENDING_FORWARD_EVENT_REQUESTS) {
 | 
			
		||||
      this._logger.warn(undefined, 'exceeded the maximum number of pending forwardEvent calls '
 | 
			
		||||
        + `(${MAX_PENDING_FORWARD_EVENT_REQUESTS}). Skipping forwarding of event ${event}.`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      this._numPendingForwardEventRequests += 1;
 | 
			
		||||
      await this._postJsonPayload(JSON.stringify({event, metadata}));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      this._logger.error(undefined, `failed to forward telemetry event ${event}`, e);
 | 
			
		||||
    } finally {
 | 
			
		||||
      this._numPendingForwardEventRequests -= 1;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _postJsonPayload(payload: string) {
 | 
			
		||||
    await fetch(this._forwardTelemetryEventsUrl, {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      headers: {
 | 
			
		||||
        'Content-Type': 'application/json',
 | 
			
		||||
      },
 | 
			
		||||
      body: payload,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import {GristDeploymentType} from 'app/common/gristUrls';
 | 
			
		||||
import {TelemetryEvent, TelemetryLevel, TelemetryMetadata} from 'app/common/Telemetry';
 | 
			
		||||
import {TelemetryEvent, TelemetryLevel} from 'app/common/Telemetry';
 | 
			
		||||
import {ILogMeta, LogMethods} from 'app/server/lib/LogMethods';
 | 
			
		||||
import {ITelemetry, Telemetry} from 'app/server/lib/Telemetry';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
@ -27,10 +27,11 @@ describe('Telemetry', function() {
 | 
			
		||||
      let installationId: string;
 | 
			
		||||
      let server: TestServer;
 | 
			
		||||
      let telemetry: ITelemetry;
 | 
			
		||||
      let forwardEventSpy: sinon.SinonSpy;
 | 
			
		||||
      let postJsonPayloadStub: sinon.SinonStub;
 | 
			
		||||
 | 
			
		||||
      const sandbox = sinon.createSandbox();
 | 
			
		||||
      const loggedEvents: [TelemetryEvent, ILogMeta][] = [];
 | 
			
		||||
      const forwardedEvents: [TelemetryEvent, TelemetryMetadata | undefined][] = [];
 | 
			
		||||
 | 
			
		||||
      before(async function() {
 | 
			
		||||
        process.env.GRIST_TEST_SERVER_DEPLOYMENT_TYPE = deploymentType;
 | 
			
		||||
@ -43,11 +44,10 @@ describe('Telemetry', function() {
 | 
			
		||||
          .callsFake((_level: string, _info: unknown, name: string, meta: ILogMeta) => {
 | 
			
		||||
            loggedEvents.push([name as TelemetryEvent, meta]);
 | 
			
		||||
          });
 | 
			
		||||
        sandbox
 | 
			
		||||
          .stub(Telemetry.prototype, 'forwardEvent')
 | 
			
		||||
          .callsFake((event: TelemetryEvent, metadata?: TelemetryMetadata) => {
 | 
			
		||||
            forwardedEvents.push([event, metadata]);
 | 
			
		||||
          });
 | 
			
		||||
        forwardEventSpy = sandbox
 | 
			
		||||
          .spy(Telemetry.prototype as any, '_forwardEvent');
 | 
			
		||||
        postJsonPayloadStub = sandbox
 | 
			
		||||
          .stub(Telemetry.prototype as any, '_postJsonPayload');
 | 
			
		||||
        telemetry = server.server.getTelemetry();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
@ -106,7 +106,7 @@ describe('Telemetry', function() {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            assert.equal(loggedEvents.length, 1);
 | 
			
		||||
            assert.isEmpty(forwardedEvents);
 | 
			
		||||
            assert.equal(forwardEventSpy.callCount, 0);
 | 
			
		||||
          });
 | 
			
		||||
        } else {
 | 
			
		||||
          it('forwards telemetry events', async function() {
 | 
			
		||||
@ -117,7 +117,7 @@ describe('Telemetry', function() {
 | 
			
		||||
                  isPublic: false,
 | 
			
		||||
                },
 | 
			
		||||
              });
 | 
			
		||||
              assert.deepEqual(forwardedEvents[forwardedEvents.length - 1], [
 | 
			
		||||
              assert.deepEqual(forwardEventSpy.lastCall.args, [
 | 
			
		||||
                'documentOpened',
 | 
			
		||||
                {
 | 
			
		||||
                  docIdDigest: 'digest',
 | 
			
		||||
@ -136,7 +136,7 @@ describe('Telemetry', function() {
 | 
			
		||||
                  userId: 1,
 | 
			
		||||
                },
 | 
			
		||||
              });
 | 
			
		||||
              assert.deepEqual(forwardedEvents[forwardedEvents.length - 1], [
 | 
			
		||||
              assert.deepEqual(forwardEventSpy.lastCall.args, [
 | 
			
		||||
                'documentOpened',
 | 
			
		||||
                {
 | 
			
		||||
                  docIdDigest: 'digest',
 | 
			
		||||
@ -146,7 +146,7 @@ describe('Telemetry', function() {
 | 
			
		||||
              ]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            assert.equal(forwardedEvents.length, 1);
 | 
			
		||||
            assert.equal(forwardEventSpy.callCount, 1);
 | 
			
		||||
            assert.isEmpty(loggedEvents);
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
@ -159,7 +159,7 @@ describe('Telemetry', function() {
 | 
			
		||||
            },
 | 
			
		||||
          });
 | 
			
		||||
          assert.isEmpty(loggedEvents);
 | 
			
		||||
          assert.isEmpty(forwardedEvents);
 | 
			
		||||
          assert.equal(forwardEventSpy.callCount, 0);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -231,7 +231,7 @@ describe('Telemetry', function() {
 | 
			
		||||
              assert.equal(loggedEvents.length, 3);
 | 
			
		||||
              assert.equal(loggedEvents[1][0], 'apiUsage');
 | 
			
		||||
            }
 | 
			
		||||
            assert.isEmpty(forwardedEvents);
 | 
			
		||||
            assert.equal(forwardEventSpy.callCount, 0);
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          if (telemetryLevel === 'limited') {
 | 
			
		||||
@ -256,7 +256,7 @@ describe('Telemetry', function() {
 | 
			
		||||
              assert.equal(metadata.watchTimeSeconds, 60);
 | 
			
		||||
              assert.equal(metadata.userId, 123);
 | 
			
		||||
              assert.equal(loggedEvents.length, 3);
 | 
			
		||||
              assert.isEmpty(forwardedEvents);
 | 
			
		||||
              assert.equal(forwardEventSpy.callCount, 0);
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
@ -267,7 +267,7 @@ describe('Telemetry', function() {
 | 
			
		||||
                limited: {watchTimeSeconds: 30},
 | 
			
		||||
              },
 | 
			
		||||
            }, chimpy);
 | 
			
		||||
            const [event, metadata] = forwardedEvents[forwardedEvents.length - 1];
 | 
			
		||||
            const [event, metadata] = forwardEventSpy.lastCall.args;
 | 
			
		||||
            assert.equal(event, 'watchedVideoTour');
 | 
			
		||||
            if (telemetryLevel === 'limited') {
 | 
			
		||||
              assert.deepEqual(metadata, {
 | 
			
		||||
@ -283,25 +283,48 @@ describe('Telemetry', function() {
 | 
			
		||||
                'userId',
 | 
			
		||||
                'altSessionId',
 | 
			
		||||
              ]);
 | 
			
		||||
              assert.equal(metadata!.watchTimeSeconds, 30);
 | 
			
		||||
              assert.equal(metadata!.userId, 1);
 | 
			
		||||
              assert.equal(metadata.watchTimeSeconds, 30);
 | 
			
		||||
              assert.equal(metadata.userId, 1);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (telemetryLevel === 'limited') {
 | 
			
		||||
              assert.equal(forwardedEvents.length, 2);
 | 
			
		||||
              assert.equal(forwardEventSpy.callCount, 2);
 | 
			
		||||
            } else {
 | 
			
		||||
              // The POST above also triggers an "apiUsage" event.
 | 
			
		||||
              assert.equal(forwardedEvents.length, 3);
 | 
			
		||||
              assert.equal(forwardedEvents[1][0], 'apiUsage');
 | 
			
		||||
              assert.equal(forwardEventSpy.callCount, 3);
 | 
			
		||||
              assert.equal(forwardEventSpy.secondCall.args[0], 'apiUsage');
 | 
			
		||||
            }
 | 
			
		||||
            assert.isEmpty(loggedEvents);
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          it('skips forwarding events if too many requests are pending', async function() {
 | 
			
		||||
            let numRequestsMade = 0;
 | 
			
		||||
            postJsonPayloadStub.callsFake(async () => {
 | 
			
		||||
              numRequestsMade += 1;
 | 
			
		||||
              await new Promise(resolve => setTimeout(resolve, 1000));
 | 
			
		||||
            });
 | 
			
		||||
            forwardEventSpy.resetHistory();
 | 
			
		||||
 | 
			
		||||
            // Log enough events simultaneously to cause some to be skipped. (The limit is 25.)
 | 
			
		||||
            for (let i = 0; i < 30; i++) {
 | 
			
		||||
              void telemetry.logEvent('documentOpened', {
 | 
			
		||||
                limited: {
 | 
			
		||||
                  docIdDigest: 'digest',
 | 
			
		||||
                  isPublic: false,
 | 
			
		||||
                },
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Check that out of the 30 forwardEvent calls, only 25 made POST requests.
 | 
			
		||||
            assert.equal(forwardEventSpy.callCount, 30);
 | 
			
		||||
            assert.equal(numRequestsMade, 25);
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        it('does not log telemetry events sent to /api/telemetry', async function() {
 | 
			
		||||
          await telemetry.logEvent('apiUsage', {limited: {method: 'GET'}});
 | 
			
		||||
          assert.isEmpty(loggedEvents);
 | 
			
		||||
          assert.isEmpty(forwardedEvents);
 | 
			
		||||
          assert.equal(forwardEventSpy.callCount, 0);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user