diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e03b8b044bfc..8178178f3683 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,7 +87,7 @@ jobs: key: ${{ runner.os }}-${{ github.sha }} - run: yarn install - name: Unit Tests - run: yarn test --ignore="@sentry/ember" + run: yarn test - uses: codecov/codecov-action@v1 job_browserstack_test: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cfde2793af1..e5047247ad1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +- [ember] feat: Add performance instrumentation for routes (#2784) ## 5.22.3 diff --git a/packages/ember/README.md b/packages/ember/README.md index 4cfb1f3f77cf..60e3dd09e041 100644 --- a/packages/ember/README.md +++ b/packages/ember/README.md @@ -62,6 +62,34 @@ Aside from configuration passed from this addon into `@sentry/browser` via the ` sentry: ... // See sentry-javascript configuration https://docs.sentry.io/error-reporting/configuration/?platform=javascript }; ``` +#### Disabling Performance + +`@sentry/ember` captures performance by default, if you would like to disable the automatic performance instrumentation, you can add the following to your `config/environment.js`: + +```javascript + ENV['@sentry/ember'] = { + disablePerformance: true, // Will disable automatic instrumentation of performance. Manual instrumentation will still be sent. + sentry: ... // See sentry-javascript configuration https://docs.sentry.io/error-reporting/configuration/?platform=javascript + }; +``` + +### Performance +#### Routes +If you would like to capture `beforeModel`, `model`, `afterModel` and `setupController` times for one of your routes, +you can import `instrumentRoutePerformance` and wrap your route with it. + +```javascript +import Route from '@ember/routing/route'; +import { instrumentRoutePerformance } from '@sentry/ember'; + +export default instrumentRoutePerformance( + class MyRoute extends Route { + model() { + //... + } + } +); +``` ### Supported Versions diff --git a/packages/ember/addon/config.d.ts b/packages/ember/addon/config.d.ts new file mode 100644 index 000000000000..193d08f2db93 --- /dev/null +++ b/packages/ember/addon/config.d.ts @@ -0,0 +1,14 @@ +declare module 'ember-get-config' { + import { BrowserOptions } from '@sentry/browser'; + type EmberSentryConfig = { + sentry: BrowserOptions; + transitionTimeout: number; + ignoreEmberOnErrorWarning: boolean; + disablePerformance: boolean; + disablePostTransitionRender: boolean; + }; + const config: { + '@sentry/ember': EmberSentryConfig; + }; + export default config; +} diff --git a/packages/ember/addon/declarations.d.ts b/packages/ember/addon/declarations.d.ts deleted file mode 100644 index fa430c829328..000000000000 --- a/packages/ember/addon/declarations.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module 'ember-get-config' { - export default object; -} \ No newline at end of file diff --git a/packages/ember/addon/index.ts b/packages/ember/addon/index.ts index 5a27e1191b5c..1069225c4934 100644 --- a/packages/ember/addon/index.ts +++ b/packages/ember/addon/index.ts @@ -37,6 +37,40 @@ export function InitSentryForEmber(_runtimeConfig: BrowserOptions | undefined) { }); } +const getCurrentTransaction = () => { + return Sentry.getCurrentHub() + ?.getScope() + ?.getTransaction(); +}; + +const instrumentFunction = async (op: string, description: string, fn: Function, args: any) => { + const currentTransaction = getCurrentTransaction(); + const span = currentTransaction?.startChild({ op, description }); + const result = await fn(...args); + span?.finish(); + return result; +}; + +export const instrumentRoutePerformance = (BaseRoute: any) => { + return class InstrumentedRoute extends BaseRoute { + beforeModel(...args: any[]) { + return instrumentFunction('ember.route.beforeModel', (this).fullRouteName, super.beforeModel, args); + } + + async model(...args: any[]) { + return instrumentFunction('ember.route.model', (this).fullRouteName, super.model, args); + } + + async afterModel(...args: any[]) { + return instrumentFunction('ember.route.afterModel', (this).fullRouteName, super.afterModel, args); + } + + async setupController(...args: any[]) { + return instrumentFunction('ember.route.setupController', (this).fullRouteName, super.setupController, args); + } + }; +}; + function createEmberEventProcessor(): void { if (addGlobalEventProcessor) { addGlobalEventProcessor(event => { diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts new file mode 100644 index 000000000000..f76b605a8fb3 --- /dev/null +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -0,0 +1,131 @@ +import ApplicationInstance from '@ember/application/instance'; +import Ember from 'ember'; +import { scheduleOnce } from '@ember/runloop'; +import environmentConfig from 'ember-get-config'; +import Sentry from '@sentry/browser'; +import { Integration } from '@sentry/types'; + +export function initialize(appInstance: ApplicationInstance): void { + const config = environmentConfig['@sentry/ember']; + if (config['disablePerformance']) { + return; + } + const performancePromise = instrumentForPerformance(appInstance); + if (Ember.testing) { + (window)._sentryPerformanceLoad = performancePromise; + } +} + +function getTransitionInformation(transition: any, router: any) { + const fromRoute = transition?.from?.name; + const toRoute = transition ? transition.to.name : router.currentRouteName; + return { + fromRoute, + toRoute, + }; +} + +export function _instrumentEmberRouter( + routerService: any, + routerMain: any, + config: typeof environmentConfig['@sentry/ember'], + startTransaction: Function, + startTransactionOnPageLoad?: boolean, +) { + const { disablePostTransitionRender } = config; + const location = routerMain.location; + let activeTransaction: any; + let transitionSpan: any; + + const url = location && location.getURL && location.getURL(); + + if (Ember.testing) { + routerService._sentryInstrumented = true; + } + + if (startTransactionOnPageLoad && url) { + const routeInfo = routerService.recognize(url); + activeTransaction = startTransaction({ + name: `route:${routeInfo.name}`, + op: 'pageload', + tags: { + url, + toRoute: routeInfo.name, + 'routing.instrumentation': '@sentry/ember', + }, + }); + } + + routerService.on('routeWillChange', (transition: any) => { + const { fromRoute, toRoute } = getTransitionInformation(transition, routerService); + activeTransaction = startTransaction({ + name: `route:${toRoute}`, + op: 'navigation', + tags: { + fromRoute, + toRoute, + 'routing.instrumentation': '@sentry/ember', + }, + }); + transitionSpan = activeTransaction.startChild({ + op: 'ember.transition', + description: `route:${fromRoute} -> route:${toRoute}`, + }); + }); + + routerService.on('routeDidChange', (transition: any) => { + const { toRoute } = getTransitionInformation(transition, routerService); + let renderSpan: any; + if (!transitionSpan || !activeTransaction) { + return; + } + transitionSpan.finish(); + + if (disablePostTransitionRender) { + activeTransaction.finish(); + } + + function startRenderSpan() { + renderSpan = activeTransaction.startChild({ + op: 'ember.runloop.render', + description: `post-transition render route:${toRoute}`, + }); + } + + function finishRenderSpan() { + renderSpan.finish(); + activeTransaction.finish(); + } + + scheduleOnce('routerTransitions', null, startRenderSpan); + scheduleOnce('afterRender', null, finishRenderSpan); + }); +} + +export async function instrumentForPerformance(appInstance: ApplicationInstance) { + const config = environmentConfig['@sentry/ember']; + const sentryConfig = config.sentry; + const tracing = await import('@sentry/tracing'); + + const idleTimeout = config.transitionTimeout || 5000; + + const existingIntegrations = (sentryConfig['integrations'] || []) as Integration[]; + + sentryConfig['integrations'] = [ + ...existingIntegrations, + new tracing.Integrations.BrowserTracing({ + routingInstrumentation: (startTransaction, startTransactionOnPageLoad) => { + const routerMain = appInstance.lookup('router:main'); + const routerService = appInstance.lookup('service:router'); + _instrumentEmberRouter(routerService, routerMain, config, startTransaction, startTransactionOnPageLoad); + }, + idleTimeout, + }), + ]; + + Sentry.init(sentryConfig); // Call init again to rebind client with new integration list in addition to the defaults +} + +export default { + initialize, +}; diff --git a/packages/ember/app/instance-initializers/sentry-performance.js b/packages/ember/app/instance-initializers/sentry-performance.js new file mode 100644 index 000000000000..3137198dc7c2 --- /dev/null +++ b/packages/ember/app/instance-initializers/sentry-performance.js @@ -0,0 +1 @@ +export { default, initialize } from '@sentry/ember/instance-initializers/sentry-performance'; diff --git a/packages/ember/index.js b/packages/ember/index.js index 2e1d1d8d5fa0..967d490abc24 100644 --- a/packages/ember/index.js +++ b/packages/ember/index.js @@ -1,5 +1,10 @@ 'use strict'; module.exports = { - name: require('./package').name + name: require('./package').name, + options: { + babel: { + plugins: [ require.resolve('ember-auto-import/babel-plugin') ] + } + } }; diff --git a/packages/ember/package.json b/packages/ember/package.json index 56cc873d1402..7bf3b7868ead 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@sentry/browser": "5.22.3", + "@sentry/tracing": "5.22.3", "@sentry/types": "5.22.3", "@sentry/utils": "5.22.3", "ember-auto-import": "^1.6.0", @@ -69,6 +70,7 @@ "ember-template-lint": "^2.9.1", "ember-test-selectors": "^4.1.0", "ember-try": "^1.4.0", + "ember-window-mock": "^0.7.1", "eslint": "7.6.0", "eslint-plugin-ember": "^8.6.0", "eslint-plugin-node": "^11.1.0", diff --git a/packages/ember/tests/acceptance/sentry-errors-test.js b/packages/ember/tests/acceptance/sentry-errors-test.js index 2c60b9c8d894..00647bf9288f 100644 --- a/packages/ember/tests/acceptance/sentry-errors-test.js +++ b/packages/ember/tests/acceptance/sentry-errors-test.js @@ -10,12 +10,16 @@ const defaultAssertOptions = { errorBodyContains: [], }; -function assertSentryEventCount(assert, count) { - assert.equal(window._sentryTestEvents.length, count, 'Check correct number of Sentry events were sent'); +function getTestSentryErrors() { + return window._sentryTestEvents.filter(event => event['type'] !== 'transaction'); +} + +function assertSentryErrorCount(assert, count) { + assert.equal(getTestSentryErrors().length, count, 'Check correct number of Sentry events were sent'); } function assertSentryCall(assert, callNumber, options) { - const sentryTestEvents = window._sentryTestEvents; + const sentryTestEvents = getTestSentryErrors(); const assertOptions = Object.assign({}, defaultAssertOptions, options); const event = sentryTestEvents[callNumber]; @@ -26,15 +30,16 @@ function assertSentryCall(assert, callNumber, options) { */ assert.ok(assertOptions.errorBodyContains.length, 'Must pass strings to check against error body'); const errorBody = JSON.stringify(event); - assertOptions.errorBodyContains.forEach((bodyContent) => { + assertOptions.errorBodyContains.forEach(bodyContent => { assert.ok(errorBody.includes(bodyContent), `Checking that error body includes ${bodyContent}`); }); } -module('Acceptance | Sentry Errors', function (hooks) { +module('Acceptance | Sentry Errors', function(hooks) { setupApplicationTest(hooks); - hooks.beforeEach(function () { + hooks.beforeEach(async function() { + await window._sentryPerformanceLoad; window._sentryTestEvents = []; const errorMessages = []; this.errorMessages = errorMessages; @@ -49,12 +54,12 @@ module('Acceptance | Sentry Errors', function (hooks) { */ this.qunitOnUnhandledRejection = sinon.stub(QUnit, 'onUnhandledRejection'); - QUnit.onError = function ({ message }) { + QUnit.onError = function({ message }) { errorMessages.push(message.split('Error: ')[1]); return true; }; - Ember.onerror = function (...args) { + Ember.onerror = function(...args) { const [error] = args; errorMessages.push(error.message); throw error; @@ -65,7 +70,7 @@ module('Acceptance | Sentry Errors', function (hooks) { * Will collect errors when run via testem in cli */ - window.onerror = function (error, ...args) { + window.onerror = function(error, ...args) { errorMessages.push(error.split('Error: ')[1]); if (this._windowOnError) { return this._windowOnError(error, ...args); @@ -73,42 +78,42 @@ module('Acceptance | Sentry Errors', function (hooks) { }; }); - hooks.afterEach(function () { + hooks.afterEach(function() { this.fetchStub.restore(); this.qunitOnUnhandledRejection.restore(); window.onerror = this._windowOnError; }); - test('Check "Throw Generic Javascript Error"', async function (assert) { + test('Check "Throw Generic Javascript Error"', async function(assert) { await visit('/'); const button = find('[data-test-button="Throw Generic Javascript Error"]'); await click(button); - assertSentryEventCount(assert, 1); + assertSentryErrorCount(assert, 1); assertSentryCall(assert, 0, { errorBodyContains: [...this.errorMessages] }); }); - test('Check "Throw EmberError"', async function (assert) { + test('Check "Throw EmberError"', async function(assert) { await visit('/'); const button = find('[data-test-button="Throw EmberError"]'); await click(button); - assertSentryEventCount(assert, 1); + assertSentryErrorCount(assert, 1); assertSentryCall(assert, 0, { errorBodyContains: [...this.errorMessages] }); }); - test('Check "Caught Thrown EmberError"', async function (assert) { + test('Check "Caught Thrown EmberError"', async function(assert) { await visit('/'); const button = find('[data-test-button="Caught Thrown EmberError"]'); await click(button); - assertSentryEventCount(assert, 0); + assertSentryErrorCount(assert, 0); }); - test('Check "Error From Fetch"', async function (assert) { + test('Check "Error From Fetch"', async function(assert) { this.fetchStub.onFirstCall().callsFake((...args) => { return this.fetchStub.callsThrough(args); }); @@ -120,41 +125,41 @@ module('Acceptance | Sentry Errors', function (hooks) { const done = assert.async(); run.next(() => { - assertSentryEventCount(assert, 1); + assertSentryErrorCount(assert, 1); assertSentryCall(assert, 0, { errorBodyContains: [...this.errorMessages] }); done(); }); }); - test('Check "Error in AfterRender"', async function (assert) { + test('Check "Error in AfterRender"', async function(assert) { await visit('/'); const button = find('[data-test-button="Error in AfterRender"]'); await click(button); - assertSentryEventCount(assert, 1); + assertSentryErrorCount(assert, 1); assert.ok(this.qunitOnUnhandledRejection.calledOnce, 'Uncaught rejection should only be called once'); assertSentryCall(assert, 0, { errorBodyContains: [...this.errorMessages] }); }); - test('Check "RSVP Rejection"', async function (assert) { + test('Check "RSVP Rejection"', async function(assert) { await visit('/'); const button = find('[data-test-button="RSVP Rejection"]'); await click(button); - assertSentryEventCount(assert, 1); + assertSentryErrorCount(assert, 1); assert.ok(this.qunitOnUnhandledRejection.calledOnce, 'Uncaught rejection should only be called once'); assertSentryCall(assert, 0, { errorBodyContains: [this.qunitOnUnhandledRejection.getCall(0).args[0]] }); }); - test('Check "Error inside RSVP"', async function (assert) { + test('Check "Error inside RSVP"', async function(assert) { await visit('/'); const button = find('[data-test-button="Error inside RSVP"]'); await click(button); - assertSentryEventCount(assert, 1); + assertSentryErrorCount(assert, 1); assert.ok(this.qunitOnUnhandledRejection.calledOnce, 'Uncaught rejection should only be called once'); assertSentryCall(assert, 0, { errorBodyContains: [...this.errorMessages] }); }); diff --git a/packages/ember/tests/acceptance/sentry-performance-test.js b/packages/ember/tests/acceptance/sentry-performance-test.js new file mode 100644 index 000000000000..312fc1a8d3cb --- /dev/null +++ b/packages/ember/tests/acceptance/sentry-performance-test.js @@ -0,0 +1,120 @@ +import { test, module } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; +import { find, click, visit } from '@ember/test-helpers'; +import Ember from 'ember'; +import sinon from 'sinon'; +import { _instrumentEmberRouter } from '@sentry/ember/instance-initializers/sentry-performance'; +import { startTransaction } from '@sentry/browser'; + +const SLOW_TRANSITION_WAIT = 3000; + +function getTestSentryTransactions() { + return window._sentryTestEvents.filter(event => event['type'] === 'transaction'); +} + +function assertSentryTransactionCount(assert, count) { + assert.equal(getTestSentryTransactions().length, count, 'Check correct number of Sentry events were sent'); +} + +function assertSentryCall(assert, callNumber, options) { + const sentryTestEvents = getTestSentryTransactions(); + + const event = sentryTestEvents[callNumber]; + assert.equal(event.spans.length, options.spanCount); + assert.equal(event.transaction, options.transaction); + assert.equal(event.tags.fromRoute, options.tags.fromRoute); + assert.equal(event.tags.toRoute, options.tags.toRoute); + + if (options.durationCheck) { + const duration = (event.timestamp - event.start_timestamp) * 1000; + assert.ok(options.durationCheck(duration), `duration (${duration}ms) didn't pass duration check`); + } +} + +module('Acceptance | Sentry Transactions', function(hooks) { + setupApplicationTest(hooks); + + hooks.beforeEach(async function() { + await window._sentryPerformanceLoad; + window._sentryTestEvents = []; + const errorMessages = []; + this.errorMessages = errorMessages; + + const routerMain = this.owner.lookup('router:main'); + const routerService = this.owner.lookup('service:router'); + + if (!routerService._sentryInstrumented) { + _instrumentEmberRouter(routerService, routerMain, {}, startTransaction); + } + + /** + * Stub out fetch function to assert on Sentry calls. + */ + this.fetchStub = sinon.stub(window, 'fetch'); + + /** + * Stops global test suite failures from unhandled rejections and allows assertion on them + */ + this.qunitOnUnhandledRejection = sinon.stub(QUnit, 'onUnhandledRejection'); + + QUnit.onError = function({ message }) { + errorMessages.push(message.split('Error: ')[1]); + return true; + }; + + Ember.onerror = function(...args) { + const [error] = args; + errorMessages.push(error.message); + throw error; + }; + + this._windowOnError = window.onerror; + + /** + * Will collect errors when run via testem in cli + */ + window.onerror = function(error, ...args) { + errorMessages.push(error.split('Error: ')[1]); + if (this._windowOnError) { + return this._windowOnError(error, ...args); + } + }; + }); + + hooks.afterEach(function() { + this.fetchStub.restore(); + this.qunitOnUnhandledRejection.restore(); + window.onerror = this._windowOnError; + }); + + test('Test transaction', async function(assert) { + await visit('/tracing'); + assertSentryTransactionCount(assert, 1); + assertSentryCall(assert, 0, { + spanCount: 2, + transaction: 'route:tracing', + tags: { + fromRoute: undefined, + toRoute: 'tracing', + }, + }); + }); + + test('Test navigating to slow route', async function(assert) { + await visit('/tracing'); + const button = find('[data-test-button="Transition to slow loading route"]'); + + await click(button); + + assertSentryTransactionCount(assert, 2); + assertSentryCall(assert, 1, { + spanCount: 2, + transaction: 'route:slow-loading-route', + durationCheck: duration => duration > SLOW_TRANSITION_WAIT, + tags: { + fromRoute: 'tracing', + toRoute: 'slow-loading-route', + }, + }); + }); +}); diff --git a/packages/ember/tests/dummy/app/app.js b/packages/ember/tests/dummy/app/app.js index 6081cb2b951f..8beb74a67fc2 100644 --- a/packages/ember/tests/dummy/app/app.js +++ b/packages/ember/tests/dummy/app/app.js @@ -20,7 +20,9 @@ class TestFetchTransport extends Transports.FetchTransport { } } -InitSentryForEmber({ transport: TestFetchTransport }); +InitSentryForEmber({ + transport: TestFetchTransport, +}); export default class App extends Application { modulePrefix = config.modulePrefix; diff --git a/packages/ember/tests/dummy/app/components/slow-loading-list.ts b/packages/ember/tests/dummy/app/components/slow-loading-list.ts new file mode 100644 index 000000000000..cd30d34acea0 --- /dev/null +++ b/packages/ember/tests/dummy/app/components/slow-loading-list.ts @@ -0,0 +1,12 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; + +export default Component.extend({ + rowItems: computed('items', function() { + return new Array(parseInt(this.items)).fill(0).map((_, index) => { + return { + index: index + 1, + }; + }); + }), +}); diff --git a/packages/ember/tests/dummy/app/controllers/application.js b/packages/ember/tests/dummy/app/controllers/application.js index 49ebe3e6cf45..304707936f81 100644 --- a/packages/ember/tests/dummy/app/controllers/application.js +++ b/packages/ember/tests/dummy/app/controllers/application.js @@ -1,63 +1,3 @@ import Controller from '@ember/controller'; -import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; -import EmberError from '@ember/error'; -import { scheduleOnce } from '@ember/runloop'; -import RSVP from 'rsvp'; -export default class ApplicationController extends Controller { - @tracked showComponents; - - @action - createError() { - this.nonExistentFunction(); - } - - @action - createEmberError() { - throw new EmberError('Whoops, looks like you have an EmberError'); - } - - @action - createCaughtEmberError() { - try { - throw new EmberError('Looks like you have a caught EmberError'); - } catch(e) { - console.log(e); - } - } - - @action - createFetchError() { - fetch('http://doesntexist.example'); - } - - @action - createAfterRenderError() { - function throwAfterRender() { - throw new Error('After Render Error'); - } - scheduleOnce('afterRender', throwAfterRender); - } - - @action - createRSVPRejection() { - const promise = new RSVP.Promise((resolve, reject) => { - reject('Promise rejected'); - }); - return promise; - } - - @action - createRSVPError() { - const promise = new RSVP.Promise(() => { - throw new Error('Error within RSVP Promise'); - }); - return promise; - } - - @action - toggleShowComponents() { - this.showComponents = !this.showComponents; - } -} +export default class ApplicationController extends Controller {} diff --git a/packages/ember/tests/dummy/app/controllers/index.js b/packages/ember/tests/dummy/app/controllers/index.js new file mode 100644 index 000000000000..a1e01de99a8c --- /dev/null +++ b/packages/ember/tests/dummy/app/controllers/index.js @@ -0,0 +1,63 @@ +import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import EmberError from '@ember/error'; +import { scheduleOnce } from '@ember/runloop'; +import RSVP from 'rsvp'; + +export default class IndexController extends Controller { + @tracked showComponents; + + @action + createError() { + this.nonExistentFunction(); + } + + @action + createEmberError() { + throw new EmberError('Whoops, looks like you have an EmberError'); + } + + @action + createCaughtEmberError() { + try { + throw new EmberError('Looks like you have a caught EmberError'); + } catch (e) { + console.log(e); + } + } + + @action + createFetchError() { + fetch('http://doesntexist.example'); + } + + @action + createAfterRenderError() { + function throwAfterRender() { + throw new Error('After Render Error'); + } + scheduleOnce('afterRender', throwAfterRender); + } + + @action + createRSVPRejection() { + const promise = new RSVP.Promise((resolve, reject) => { + reject('Promise rejected'); + }); + return promise; + } + + @action + createRSVPError() { + const promise = new RSVP.Promise(() => { + throw new Error('Error within RSVP Promise'); + }); + return promise; + } + + @action + toggleShowComponents() { + this.showComponents = !this.showComponents; + } +} diff --git a/packages/ember/tests/dummy/app/controllers/slow-loading-route.js b/packages/ember/tests/dummy/app/controllers/slow-loading-route.js new file mode 100644 index 000000000000..afe644bffd41 --- /dev/null +++ b/packages/ember/tests/dummy/app/controllers/slow-loading-route.js @@ -0,0 +1,9 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; + +export default class SlowLoadingRouteController extends Controller { + @action + back() { + this.transitionToRoute('tracing'); + } +} diff --git a/packages/ember/tests/dummy/app/controllers/tracing.js b/packages/ember/tests/dummy/app/controllers/tracing.js new file mode 100644 index 000000000000..539a989ac829 --- /dev/null +++ b/packages/ember/tests/dummy/app/controllers/tracing.js @@ -0,0 +1,9 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; + +export default class TracingController extends Controller { + @action + navigateToSlowRoute() { + return this.transitionToRoute('slow-loading-route'); + } +} diff --git a/packages/ember/tests/dummy/app/helpers/utils.js b/packages/ember/tests/dummy/app/helpers/utils.js new file mode 100644 index 000000000000..7aa94999942c --- /dev/null +++ b/packages/ember/tests/dummy/app/helpers/utils.js @@ -0,0 +1,3 @@ +export default function timeout(time) { + return new Promise(resolve => setTimeout(resolve, time)); +} diff --git a/packages/ember/tests/dummy/app/router.js b/packages/ember/tests/dummy/app/router.js index 224ca426a85e..1eb50a98d851 100644 --- a/packages/ember/tests/dummy/app/router.js +++ b/packages/ember/tests/dummy/app/router.js @@ -7,4 +7,6 @@ export default class Router extends EmberRouter { } Router.map(function() { + this.route('tracing'); + this.route('slow-loading-route'); }); diff --git a/packages/ember/tests/dummy/app/routes/slow-loading-route.js b/packages/ember/tests/dummy/app/routes/slow-loading-route.js new file mode 100644 index 000000000000..678520c9c202 --- /dev/null +++ b/packages/ember/tests/dummy/app/routes/slow-loading-route.js @@ -0,0 +1,25 @@ +import Route from '@ember/routing/route'; +import timeout from '../helpers/utils'; +import { instrumentRoutePerformance } from '@sentry/ember'; + +const SLOW_TRANSITION_WAIT = 3000; + +export default instrumentRoutePerformance( + class SlowLoadingRoute extends Route { + beforeModel() { + return timeout(SLOW_TRANSITION_WAIT / 3); + } + + model() { + return timeout(SLOW_TRANSITION_WAIT / 3); + } + + afterModel() { + return timeout(SLOW_TRANSITION_WAIT / 3); + } + + setupController() { + super.setupController(); + } + }, +); diff --git a/packages/ember/tests/dummy/app/styles/app.css b/packages/ember/tests/dummy/app/styles/app.css index 1f8593255fdf..4c69b62ced41 100644 --- a/packages/ember/tests/dummy/app/styles/app.css +++ b/packages/ember/tests/dummy/app/styles/app.css @@ -17,7 +17,7 @@ body { background-repeat: repeat; height: 100%; margin: 0; - font-family: Rubik,Avenir Next,Helvetica Neue,sans-serif; + font-family: Rubik, Avenir Next, Helvetica Neue, sans-serif; font-size: 16px; line-height: 24px; color: var(--foreground-color); @@ -43,7 +43,7 @@ body { .box { background-color: #fff; border: 0; - box-shadow: 0 0 0 1px rgba(0,0,0,.08),0 1px 4px rgba(0,0,0,.1); + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08), 0 1px 4px rgba(0, 0, 0, 0.1); border-radius: 4px; display: flex; width: 100%; @@ -54,8 +54,8 @@ body { padding-top: 20px; width: 60px; background: #564f64; - background-image: linear-gradient(-180deg,rgba(52,44,62,0),rgba(52,44,62,.5)); - box-shadow: 0 2px 0 0 rgba(0,0,0,.1); + background-image: linear-gradient(-180deg, rgba(52, 44, 62, 0), rgba(52, 44, 62, 0.5)); + box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.1); border-radius: 4px 0 0 4px; margin-top: -1px; margin-bottom: -1px; @@ -73,12 +73,37 @@ body { background-image: url('/assets/images/sentry-logo.svg'); } +.nav { + display: flex; + justify-content: center; + padding: 10px; + padding-top: 20px; + padding-bottom: 0px; +} + +.nav a { + padding-left: 10px; + padding-right: 10px; + font-weight: 500; + text-decoration: none; + color: var(--foreground-color); +} + +.nav a.active { + border-bottom: 4px solid #6c5fc7; +} + section.content { flex: 1; padding-bottom: 40px; } -h1, h2, h3, h4, h5, h6 { +h1, +h2, +h3, +h4, +h5, +h6 { font-weight: 600; } @@ -101,7 +126,8 @@ div.section { padding-top: 20px; } -.content-container h3, .content-container h4 { +.content-container h3, +.content-container h4 { margin-top: 0px; } @@ -113,7 +139,7 @@ button { border-radius: 3px; font-weight: 600; padding: 8px 16px; - transition: all .1s; + transition: all 0.1s; border: 1px solid transparent; border-radius: 3px; @@ -139,8 +165,8 @@ button.primary { display: inline-block; - text-shadow: 0 -1px 0 rgba(0,0,0,.15); - box-shadow: 0 2px 0 rgba(0,0,0,.08); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: 0 2px 0 rgba(0, 0, 0, 0.08); text-transform: none; overflow: visible; @@ -154,7 +180,6 @@ button.primary:hover { button.primary:focus { background: #5b4cc0; border-color: #3a2f87; - box-shadow: inset 0 2px 0 rgba(0,0,0,.12); + box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.12); outline: none; } - diff --git a/packages/ember/tests/dummy/app/templates/application.hbs b/packages/ember/tests/dummy/app/templates/application.hbs index 6dac270780f8..4e41d992dc3c 100644 --- a/packages/ember/tests/dummy/app/templates/application.hbs +++ b/packages/ember/tests/dummy/app/templates/application.hbs @@ -1,4 +1,3 @@ -
@@ -9,46 +8,14 @@

Sentry Instrumented Ember Application

+
- - - - - - - + {{outlet}}
- -{{outlet}} diff --git a/packages/ember/tests/dummy/app/templates/components/slow-loading-list.hbs b/packages/ember/tests/dummy/app/templates/components/slow-loading-list.hbs new file mode 100644 index 000000000000..8dce1ce0c319 --- /dev/null +++ b/packages/ember/tests/dummy/app/templates/components/slow-loading-list.hbs @@ -0,0 +1,10 @@ +
+

Slow Loading List

+
+ {{#each this.rowItems as |rowItem|}} +
+ {{rowItem.index}} +
+ {{/each}} +
+
diff --git a/packages/ember/tests/dummy/app/templates/index.hbs b/packages/ember/tests/dummy/app/templates/index.hbs new file mode 100644 index 000000000000..b39ffce5c0e8 --- /dev/null +++ b/packages/ember/tests/dummy/app/templates/index.hbs @@ -0,0 +1,11 @@ + + + + + + + +{{outlet}} diff --git a/packages/ember/tests/dummy/app/templates/slow-loading-route.hbs b/packages/ember/tests/dummy/app/templates/slow-loading-route.hbs new file mode 100644 index 000000000000..a1177d563500 --- /dev/null +++ b/packages/ember/tests/dummy/app/templates/slow-loading-route.hbs @@ -0,0 +1,6 @@ +

Intentionally Slow Route

+ + + diff --git a/packages/ember/tests/dummy/app/templates/tracing.hbs b/packages/ember/tests/dummy/app/templates/tracing.hbs new file mode 100644 index 000000000000..b2e5087f9f3e --- /dev/null +++ b/packages/ember/tests/dummy/app/templates/tracing.hbs @@ -0,0 +1,2 @@ + diff --git a/packages/ember/tests/dummy/config/environment.js b/packages/ember/tests/dummy/config/environment.js index bb0f5c2608c6..e8588c35c079 100644 --- a/packages/ember/tests/dummy/config/environment.js +++ b/packages/ember/tests/dummy/config/environment.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = function (environment) { +module.exports = function(environment) { let ENV = { modulePrefix: 'dummy', environment, @@ -25,6 +25,7 @@ module.exports = function (environment) { ENV['@sentry/ember'] = { sentry: { + tracesSampleRate: 1, dsn: process.env.SENTRY_DSN, }, ignoreEmberOnErrorWarning: true, diff --git a/packages/ember/tests/dummy/constants.js b/packages/ember/tests/dummy/constants.js new file mode 100644 index 000000000000..5b1d70dc2722 --- /dev/null +++ b/packages/ember/tests/dummy/constants.js @@ -0,0 +1 @@ +export const SLOW_TRANSITION_WAIT = 3000; // Make dummy route wait 3000ms diff --git a/packages/ember/tests/test-helper.js b/packages/ember/tests/test-helper.js index 2a1ea0da4641..69387ded60fb 100644 --- a/packages/ember/tests/test-helper.js +++ b/packages/ember/tests/test-helper.js @@ -1,5 +1,6 @@ import sinon from 'sinon'; import * as Sentry from '@sentry/browser'; +import environmentConfig from 'ember-get-config'; /** * Stub Sentry init function before application is imported to avoid actually setting up Sentry and needing a DSN @@ -10,6 +11,23 @@ import Application from '../app'; import config from '../config/environment'; import { setApplication } from '@ember/test-helpers'; import { start } from 'ember-qunit'; +import { Transports } from '@sentry/browser'; +import Ember from 'ember'; + +export class TestFetchTransport extends Transports.FetchTransport { + sendEvent(event) { + if (Ember.testing) { + if (!window._sentryTestEvents) { + window._sentryTestEvents = []; + } + window._sentryTestEvents.push(event); + return Promise.resolve(); + } + return super.sendEvent(event); + } +} + +environmentConfig['@sentry/ember'].sentry['transport'] = TestFetchTransport; setApplication(Application.create(config.APP)); diff --git a/packages/ember/tsconfig.json b/packages/ember/tsconfig.json index 71c52a3c72d9..2011ce8f8735 100644 --- a/packages/ember/tsconfig.json +++ b/packages/ember/tsconfig.json @@ -12,7 +12,7 @@ "noEmitOnError": false, "noEmit": true, "baseUrl": ".", - "module": "es6", + "module": "esnext", "experimentalDecorators": true, "paths": { "dummy/tests/*": ["tests/*"], diff --git a/scripts/test.sh b/scripts/test.sh index 723d87641ece..15c2a1f1c9b0 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -20,5 +20,5 @@ elif [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -le 8 ]]; then else yarn install yarn build - yarn test --ignore="@sentry/ember" + yarn test fi