Skip to content

Commit

Permalink
feat: add several more metrics and info
Browse files Browse the repository at this point in the history
  • Loading branch information
renanccastro committed Feb 18, 2024
1 parent ee888e0 commit def94bb
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 4 deletions.
18 changes: 18 additions & 0 deletions lib/client/hijack/wrapDynamicImport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Ntp } from '../../ntp';
const meteorInstall = require('meteor/modules').meteorInstall;
const oldFetch = meteorInstall.fetch;

export function wrapDynamicImport () {
meteorInstall.fetch = function (ids) {
const promise = oldFetch(ids);
Kadira.webVitals.numberOfImports += 1;
const now = Ntp._now();
return promise.then((...args) => {
Kadira.webVitals.importTime.push(Ntp._now() - now);
return Promise.resolve(...args);
});
};
}
export function unwrapDynamicImport () {
meteorInstall.fetch = oldFetch;
}
16 changes: 16 additions & 0 deletions lib/client/hijack/wrapLogin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Ntp } from '../../ntp';

let tracking = true;
export function wrapLogin () {
if (Package['accounts-base']) {
Package['accounts-base'].Accounts.onLogin(() => {
if (!tracking) {
return;
}
Kadira.webVitals.loggedIn = Ntp._now() - Kadira.webVitals.startTime;
});
}
}
export function unWrapLogin () {
tracking = false;
}
25 changes: 25 additions & 0 deletions lib/client/hijack/wrapMethodCall.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Meteor } from 'meteor/meteor';
import { Ntp } from '../../ntp';
const oldCall = Meteor.call;

export function wrapMethodCall () {
Meteor.call = function (name, /* .. [arguments] .. callback */) {
// if it's a function, the last argument is the result callback,
// not a parameter to the remote method.
const args = [...arguments].slice(1);
let callback;
if (args.length && typeof args[args.length - 1] === 'function') {
callback = args.pop();
}
const now = Ntp._now();

const newCallback = (...params) => {
Kadira.webVitals.methods.push(Ntp._now() - now);
callback?.(...params);
};
return this.apply(name, args, newCallback);
}.bind(Meteor.connection);
}
export function unwrapMethodCall () {
Meteor.call = oldCall;
}
39 changes: 39 additions & 0 deletions lib/client/hijack/wrapSubscription.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Meteor } from 'meteor/meteor';
import { Ntp } from '../../ntp';
const oldSubscribe = Meteor.subscribe;

export function wrapSubscription () {
Meteor.subscribe = function (/* name, .. [arguments] .. (callback|callbacks) */) {
const params = [...arguments].slice(1);
let callbacks = Object.create(null);
const now = Ntp._now();

if (params.length) {
const lastParam = params[params.length - 1];
if (typeof lastParam === 'function') {
callbacks.onReady = params.pop();
} else if (lastParam && [
lastParam.onReady,
// XXX COMPAT WITH 1.0.3.1 onError used to exist, but now we use
// onStop with an error callback instead.
lastParam.onError,
lastParam.onStop
].some(f => typeof f === 'function')) {
callbacks = params.pop();
}
}
const oldReady = callbacks.onReady;
const onReady = () => {
const diff = Ntp._now() - now;
if (diff > 0) {
Kadira.webVitals.subs.push(diff);
}
oldReady?.();
};
callbacks.onReady = onReady;
return oldSubscribe(arguments[0],...params, callbacks);
}.bind(Meteor.connection);

Check failure on line 35 in lib/client/hijack/wrapSubscription.js

View workflow job for this annotation

GitHub Actions / build

The function binding is unnecessary
}
export function unwrapSubscription () {
Meteor.subscribe = oldSubscribe;
}
7 changes: 6 additions & 1 deletion lib/client/kadira.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { Ntp } from '../ntp';
import { getBrowserInfo } from './utils';
import { httpRequest } from './httpRequest';
import { WebVitalsModel } from './models/webVitals';
import { wrapDynamicImport } from './hijack/wrapDynamicImport';
import { wrapSubscription } from './hijack/wrapSubscription';
import { wrapMethodCall } from './hijack/wrapMethodCall';

Kadira.enableErrorTracking = function () {
Kadira.options.enableErrorTracking = true;
Expand Down Expand Up @@ -58,7 +61,9 @@ function initialize (options = {}) {
return;
}
initialized = true;

wrapDynamicImport();
wrapSubscription();
wrapMethodCall();
Kadira.options = {
errorDumpInterval: 1000 * 60,
maxErrorsPerInterval: 10,
Expand Down
72 changes: 69 additions & 3 deletions lib/client/models/webVitals.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
import { getClientArchVersion } from '../../common/utils';
import { getClientArch, getBrowserInfo } from '../utils';
import { Tracker } from 'meteor/tracker';
import { onCLS, onFCP, onFID, onINP, onLCP, onTTFB } from 'web-vitals';
import { getClientArchVersion } from '../../common/utils';
import { Ntp } from '../../ntp';
import { unwrapDynamicImport } from '../hijack/wrapDynamicImport';
import { unwrapMethodCall } from '../hijack/wrapMethodCall';
import { unwrapSubscription } from '../hijack/wrapSubscription';
import { getBrowserInfo, getClientArch } from '../utils';
import { unWrapLogin, wrapLogin } from '../hijack/wrapLogin';
const average = arr => arr.reduce( ( p, c ) => p + c, 0 ) / arr.length || 0;

export class WebVitalsModel {
startTime = Ntp._now();
connectionTime = 0;
loggedIn = 0;
importTime = [];
methods = [];
subs = [];
queue = new Set();

addToQueue (metric) {
this.queue.add({ metric: metric.value, matricName: metric.name });
}
Expand All @@ -23,6 +37,36 @@ export class WebVitalsModel {
onLCP(bindedAddToQueue);
onTTFB(bindedAddToQueue);
onCLS(bindedAddToQueue);

Tracker.autorun((computation) => {
const {connected} = Meteor.status();
if (connected) {
this.connectionTime = Ntp._now() - this.startTime;
computation.stop();
}
});


// startup hooks run after the page is loaded
Meteor.startup(() => {
wrapLogin();

// wait until all startup hooks called (they're called synchronously all at once on the client)
setTimeout(() => {
/* calling unwrap before the document readyState is complete
is not the best for react based apps as it's not guaranteed the components are mounted
and hooks are called making the subscription */
document.onreadystatechange = function () {
if (document.readyState === 'complete') {
/* stop tracking new dynamic imports/methods/publications */
unwrapDynamicImport();
unwrapSubscription();
unwrapMethodCall();
unWrapLogin();
}
};
});
});
// Report all available metrics whenever the page is backgrounded or unloaded.
addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
Expand All @@ -33,8 +77,9 @@ export class WebVitalsModel {
// NOTE: Safari does not reliably fire the `visibilitychange` event when the
// page is being unloaded. If Safari support is needed, you should also flush
// the queue in the `pagehide` event.
addEventListener('pagehide', () => this.flushQueue());
addEventListener('pagehide', this.flushQueue.bind(this));
}

constructor (options) {
options = options || {};

Expand All @@ -43,6 +88,26 @@ export class WebVitalsModel {
_buildPayload = function (metrics) {
const arch = getClientArch();
const browserInfo = getBrowserInfo();
metrics.push({
metric: this.connectionTime,
metricName: 'connectionTime'
});
metrics.push({
metric: this.loggedIn,
metricName: 'loginTime'
});
metrics.push({
metric: average(this.importTime),
metricName: 'dynamicImportTime'
});
metrics.push({
metric: average(this.methods),
metricName: 'methods'
});
metrics.push({
metric: average(this.subs),
metricName: 'subs'
});

return {
host: Kadira.options.hostname,
Expand All @@ -51,6 +116,7 @@ export class WebVitalsModel {
...browserInfo,
arch,
legacy: arch.endsWith('.legacy'),
commitHash: Meteor.gitCommitHash,
cacheCleaned: !localStorage?.length && !sessionStorage?.length,
archVersion: getClientArchVersion(arch),
};
Expand Down

0 comments on commit def94bb

Please sign in to comment.