diff --git a/packages/apm/package.json b/packages/apm/package.json index c14868ff36f5..ab0b7559d10a 100644 --- a/packages/apm/package.json +++ b/packages/apm/package.json @@ -21,7 +21,8 @@ "@sentry/minimal": "5.16.1", "@sentry/types": "5.16.1", "@sentry/utils": "5.16.1", - "tslib": "^1.9.3" + "tslib": "^1.9.3", + "web-vitals": "^0.2.2" }, "devDependencies": { "@types/express": "^4.17.1", diff --git a/packages/apm/src/integrations/tracing.ts b/packages/apm/src/integrations/tracing.ts index 72a19de83d55..4b323266a842 100644 --- a/packages/apm/src/integrations/tracing.ts +++ b/packages/apm/src/integrations/tracing.ts @@ -10,6 +10,8 @@ import { timestampWithMs, } from '@sentry/utils'; +import { getCLS, getFID, getLCP, getFCP, getTTFB } from 'web-vitals'; + import { Span as SpanClass } from '../span'; import { SpanStatus } from '../spanstatus'; import { Transaction } from '../transaction'; @@ -155,6 +157,9 @@ export class Tracing implements Integration { private static _heartbeatCounter: number = 0; + /** Holds Web Vitals metrics. */ + private static _web_vitals: { [key: string]: { [key: string]: any } } = {}; + /** * Constructor for Tracing * @@ -163,6 +168,40 @@ export class Tracing implements Integration { public constructor(_options?: Partial) { if (global.performance) { global.performance.mark('sentry-tracing-init'); + + // Core Web Vitals + getCLS(({ name, entries, value }) => { + Tracing._web_vitals[name] = { + value, + }; + }); + getFID(({ name, entries, value }) => { + Tracing._web_vitals[name] = { + value, + }; + }); + getLCP(({ name, entries, value }) => { + const lastEntry = entries[entries.length - 1]; + Tracing._web_vitals[name] = { + // @ts-ignore + ...(lastEntry.id && { elementId: lastEntry.id }), + // @ts-ignore + ...(lastEntry.size && { elementSize: lastEntry.size }), + value, + }; + }, true /* reportAllChanges, necessary to be able to send the last seen LCP before the pageload transaction is sent */); + + // Other Web Vitals + getFCP(({ name, entries, value }) => { + Tracing._web_vitals[name] = { + value, + }; + }); + getTTFB(({ name, entries, value }) => { + Tracing._web_vitals[name] = { + value, + }; + }); } const defaults = { debug: { @@ -447,7 +486,7 @@ export class Tracing implements Integration { } /** - * Finshes the current active transaction + * Finishes the current active transaction */ public static finishIdleTransaction(endTimestamp: number): void { const active = Tracing._activeTransaction; @@ -505,6 +544,16 @@ export class Tracing implements Integration { Tracing._log('[Tracing] Adding & adjusting spans using Performance API'); + // FIXME: depending on the 'op' directly is brittle. + if (transactionSpan.op === 'pageload') { + if (Tracing._web_vitals) { + // Add the last observed Web Vitals metrics to the transaction. Note + // that some of the metrics may not yet be final, but we at the moment + // do not wait for them to become final. + transactionSpan.setData('_sentry_web_vitals', Tracing._web_vitals); + } + } + const timeOrigin = Tracing._msToSec(performance.timeOrigin); // tslint:disable-next-line: completed-docs diff --git a/yarn.lock b/yarn.lock index abf8e70b088b..64b1dfe0afb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1690,11 +1690,32 @@ after@0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" -agent-base@4, agent-base@5, agent-base@6, agent-base@^4.3.0, agent-base@~4.2.0: +agent-base@4, agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + +agent-base@5: version "5.1.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== +agent-base@6: + version "6.0.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.0.tgz#5d0101f19bbfaed39980b22ae866de153b93f09a" + integrity sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw== + dependencies: + debug "4" + +agent-base@~4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" + integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== + dependencies: + es6-promisify "^5.0.0" + agentkeepalive@^3.4.1: version "3.5.2" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67" @@ -4361,6 +4382,18 @@ es-to-primitive@^1.1.1, es-to-primitive@^1.2.0: is-date-object "^1.0.1" is-symbol "^1.0.2" +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -11367,6 +11400,11 @@ wcwidth@^1.0.0, wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +web-vitals@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-0.2.2.tgz#365d361590bf1a707c484d1196bd3c69e65523e0" + integrity sha512-6xR6kxa70XXnSHV4sZMDXKPvcrUfl2xaNUN1ENedcDbvcinzlWgaDD5Hn5mAnfHfKZVlSHe2/XCXKweuRnfWqw== + webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"