From 23b37b6943e12ee2de50266d90af66334ba092f2 Mon Sep 17 00:00:00 2001 From: Arthur Knaus Date: Thu, 31 Aug 2023 14:07:26 +0200 Subject: [PATCH 1/3] feat(onboarding): Add profiling to platform node-express --- .../onboarding/productSelection.tsx | 1 + .../app/gettingStartedDocs/node/express.tsx | 150 ++++++++++-------- 2 files changed, 85 insertions(+), 66 deletions(-) diff --git a/static/app/components/onboarding/productSelection.tsx b/static/app/components/onboarding/productSelection.tsx index 2dc684504c26..ae0e023ee2d3 100644 --- a/static/app/components/onboarding/productSelection.tsx +++ b/static/app/components/onboarding/productSelection.tsx @@ -65,6 +65,7 @@ export const platformProductAvailability = { ], node: [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'node-connect': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'node-express': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], python: [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-aiohttp': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-awslambda': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], diff --git a/static/app/gettingStartedDocs/node/express.tsx b/static/app/gettingStartedDocs/node/express.tsx index c6ed276ccfd8..922e95d918af 100644 --- a/static/app/gettingStartedDocs/node/express.tsx +++ b/static/app/gettingStartedDocs/node/express.tsx @@ -1,44 +1,44 @@ import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; -import {PlatformKey} from 'sentry/data/platformCategories'; import {t, tct} from 'sentry/locale'; -import type {Organization} from 'sentry/types'; - -type StepProps = { - newOrg: boolean; - organization: Organization; - platformKey: PlatformKey; - projectId: string; - sentryInitContent: string; -}; +import { + getDefaultInitParams, + getDefaultNodeImports, + getInstallSnippet, + getProductInitParams, + getProductIntegrations, + getProductSelectionMap, + joinWithIndentation, +} from 'sentry/utils/gettingStartedDocs/node'; + +interface StepProps { + hasPerformanceMonitoring: boolean; + importContent: string; + initContent: string; + installSnippet: string; +} const performanceIntegrations: string[] = [ - `// enable HTTP calls tracing -new Sentry.Integrations.Http({ tracing: true }),`, - `// enable Express.js middleware tracing -new Sentry.Integrations.Express({ app }),`, + '// enable HTTP calls tracing', + 'new Sentry.Integrations.Http({ tracing: true }),', + '// enable Express.js middleware tracing', + 'new Sentry.Integrations.Express({ app }),', ]; -const performanceOtherConfig = `// Performance Monitoring -tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production!`; - export const steps = ({ - sentryInitContent, -}: Partial = {}): LayoutProps['steps'] => [ + installSnippet, + importContent, + initContent, + hasPerformanceMonitoring, +}: StepProps): LayoutProps['steps'] => [ { type: StepType.INSTALL, description: t('Add the Sentry Node SDK as a dependency:'), configurations: [ { language: 'bash', - code: ` -# Using yarn -yarn add @sentry/node - -# Using npm -npm install --save @sentry/node - `, + code: installSnippet, }, ], }, @@ -56,40 +56,41 @@ npm install --save @sentry/node { language: 'javascript', code: ` - import * as Sentry from "@sentry/node"; - import express from "express"; +${importContent} - // or using CommonJS - // const Sentry = require('@sentry/node'); - // const express = require('express'); +const app = express(); - const app = express(); +Sentry.init({ +${initContent} +}); - Sentry.init({ - ${sentryInitContent}, - }); +// The request handler must be the first middleware on the app +app.use(Sentry.Handlers.requestHandler());${ + hasPerformanceMonitoring + ? ` - // Trace incoming requests - app.use(Sentry.Handlers.requestHandler()); - app.use(Sentry.Handlers.tracingHandler()); +// TracingHandler creates a trace for every incoming request +app.use(Sentry.Handlers.tracingHandler());` + : '' + } - // All your controllers should live here - app.get("/", function rootHandler(req, res) { - res.end("Hello world!"); - }); +// All your controllers should live here +app.get("/", function rootHandler(req, res) { + res.end("Hello world!"); +}); - // The error handler must be registered before any other error middleware and after all controllers - app.use(Sentry.Handlers.errorHandler()); +// The error handler must be registered before any other error middleware and after all controllers +app.use(Sentry.Handlers.errorHandler()); - // Optional fallthrough error handler - app.use(function onError(err, req, res, next) { - // The error id is attached to \`res.sentry\` to be returned - // and optionally displayed to the user for support. - res.statusCode = 500; - res.end(res.sentry + "\\n"); - }); +// Optional fallthrough error handler +app.use(function onError(err, req, res, next) { + // The error id is attached to \`res.sentry\` to be returned + // and optionally displayed to the user for support. + res.statusCode = 500; + res.end(res.sentry + "\\n"); +}); - app.listen(3000); +app.listen(3000); `, }, ], @@ -112,24 +113,41 @@ npm install --save @sentry/node }, ]; -export function GettingStartedWithExpress({dsn, newOrg, platformKey}: ModuleProps) { - let sentryInitContent: string[] = [`dsn: "${dsn}",`]; - - const integrations = [...performanceIntegrations]; - const otherConfigs = [performanceOtherConfig]; - - if (integrations.length > 0) { - sentryInitContent = sentryInitContent.concat('integrations: [', integrations, '],'); - } - - if (otherConfigs.length > 0) { - sentryInitContent = sentryInitContent.concat(otherConfigs); - } +export function GettingStartedWithExpress({ + dsn, + newOrg, + platformKey, + activeProductSelection = [], +}: ModuleProps) { + const productSelection = getProductSelectionMap(activeProductSelection); + + const installSnippet = getInstallSnippet({productSelection}); + const imports = getDefaultNodeImports({productSelection}); + imports.push('import express from "express";'); + + const integrations = [ + ...(productSelection['performance-monitoring'] ? performanceIntegrations : []), + ...getProductIntegrations({productSelection}), + ]; + + const integrationParam = + integrations.length > 0 + ? `integrations: [\n${joinWithIndentation(integrations)}\n],` + : null; + + const initContent = joinWithIndentation([ + ...getDefaultInitParams({dsn}), + ...(integrationParam ? [integrationParam] : []), + ...getProductInitParams({productSelection}), + ]); return ( Date: Thu, 31 Aug 2023 14:27:34 +0200 Subject: [PATCH 2/3] feat(onboarding): Add profiling to platform node-koa --- .../onboarding/productSelection.tsx | 1 + static/app/gettingStartedDocs/node/koa.tsx | 274 ++++++++++-------- static/app/utils/gettingStartedDocs/node.ts | 10 +- 3 files changed, 158 insertions(+), 127 deletions(-) diff --git a/static/app/components/onboarding/productSelection.tsx b/static/app/components/onboarding/productSelection.tsx index ae0e023ee2d3..7ed1150922dd 100644 --- a/static/app/components/onboarding/productSelection.tsx +++ b/static/app/components/onboarding/productSelection.tsx @@ -66,6 +66,7 @@ export const platformProductAvailability = { node: [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'node-connect': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'node-express': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'node-koa': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], python: [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-aiohttp': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-awslambda': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], diff --git a/static/app/gettingStartedDocs/node/koa.tsx b/static/app/gettingStartedDocs/node/koa.tsx index 5a767437f416..115105690a3d 100644 --- a/static/app/gettingStartedDocs/node/koa.tsx +++ b/static/app/gettingStartedDocs/node/koa.tsx @@ -1,42 +1,42 @@ import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; -import {PlatformKey} from 'sentry/data/platformCategories'; import {t, tct} from 'sentry/locale'; -import {Organization} from 'sentry/types'; - -type StepProps = { - newOrg: boolean; - organization: Organization; - platformKey: PlatformKey; - projectId: string; - sentryInitContent: string; -}; +import { + getDefaultInitParams, + getDefaultNodeImports, + getInstallSnippet, + getProductInitParams, + getProductIntegrations, + getProductSelectionMap, + joinWithIndentation, +} from 'sentry/utils/gettingStartedDocs/node'; + +interface StepProps { + hasPerformanceMonitoring: boolean; + importContent: string; + initContent: string; + installSnippet: string; +} const performanceIntegrations: string[] = [ - `// Automatically instrument Node.js libraries and frameworks - ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(),`, + '// Automatically instrument Node.js libraries and frameworks', + '...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(),', ]; -const performanceOtherConfig = `// Performance Monitoring -tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production!`; - export const steps = ({ - sentryInitContent, -}: Partial = {}): LayoutProps['steps'] => [ + installSnippet, + importContent, + initContent, + hasPerformanceMonitoring, +}: StepProps): LayoutProps['steps'] => [ { type: StepType.INSTALL, description: t('Add the Sentry Node SDK as a dependency:'), configurations: [ { language: 'bash', - code: ` -# Using yarn -yarn add @sentry/node @sentry/utils - -# Using npm -npm install --save @sentry/node @sentry/utils - `, + code: installSnippet, }, ], }, @@ -54,95 +54,98 @@ npm install --save @sentry/node @sentry/utils { language: 'javascript', code: ` - const Sentry = require("@sentry/node"); - const { stripUrlQueryAndFragment } = require("@sentry/utils"); - const Koa = require("koa"); - const app = new Koa(); +${importContent} + +const app = new Koa(); + +Sentry.init({ +${initContent} +});${ + hasPerformanceMonitoring + ? ` + +const requestHandler = (ctx, next) => { + return new Promise((resolve, reject) => { + Sentry.runWithAsyncContext(async () => { + const hub = Sentry.getCurrentHub(); + hub.configureScope((scope) => + scope.addEventProcessor((event) => + Sentry.addRequestDataToEvent(event, ctx.request, { + include: { + user: false, + }, + }) + ) + ); + + try { + await next(); + } catch (err) { + reject(err); + } + resolve(); + }); + }); +}; - Sentry.init({ - ${sentryInitContent}, - }); +// This tracing middleware creates a transaction per request +const tracingMiddleWare = async (ctx, next) => { + const reqMethod = (ctx.method || "").toUpperCase(); + const reqUrl = ctx.url && stripUrlQueryAndFragment(ctx.url); + + // Connect to trace of upstream app + let traceparentData; + if (ctx.request.get("sentry-trace")) { + traceparentData = Sentry.extractTraceparentData( + ctx.request.get("sentry-trace") + ); + } - const requestHandler = (ctx, next) => { - return new Promise((resolve, reject) => { - Sentry.runWithAsyncContext(async () => { - const hub = Sentry.getCurrentHub(); - hub.configureScope((scope) => - scope.addEventProcessor((event) => - Sentry.addRequestDataToEvent(event, ctx.request, { - include: { - user: false, - }, - }) - ) - ); - - try { - await next(); - } catch (err) { - reject(err); - } - resolve(); - }); - }); - }; - - // this tracing middleware creates a transaction per request - const tracingMiddleWare = async (ctx, next) => { - const reqMethod = (ctx.method || "").toUpperCase(); - const reqUrl = ctx.url && stripUrlQueryAndFragment(ctx.url); - - // connect to trace of upstream app - let traceparentData; - if (ctx.request.get("sentry-trace")) { - traceparentData = Sentry.extractTraceparentData( - ctx.request.get("sentry-trace") - ); - } - - const transaction = Sentry.startTransaction({ - name: \`\${reqMethod} \${reqUrl}\`, - op: "http.server", - ...traceparentData, - }); - - ctx.__sentry_transaction = transaction; - - // We put the transaction on the scope so users can attach children to it - Sentry.getCurrentHub().configureScope((scope) => { - scope.setSpan(transaction); - }); - - ctx.res.on("finish", () => { - // Push \`transaction.finish\` to the next event loop so open spans have a chance to finish before the transaction closes - setImmediate(() => { - // if you're using koa router, set the matched route as transaction name - if (ctx._matchedRoute) { - const mountPath = ctx.mountPath || ""; - transaction.setName(\`\${reqMethod} \${mountPath}\${ctx._matchedRoute}\`); - } - transaction.setHttpStatus(ctx.status); - transaction.finish(); - }); - }); - - await next(); - }; - - app.use(requestHandler); - app.use(tracingMiddleWare); - - // usual error handler - app.on("error", (err, ctx) => { - Sentry.withScope((scope) => { - scope.addEventProcessor((event) => { - return Sentry.addRequestDataToEvent(event, ctx.request); - }); - Sentry.captureException(err); - }); - }); + const transaction = Sentry.startTransaction({ + name: \`\${reqMethod} \${reqUrl}\`, + op: "http.server", + ...traceparentData, + }); + + ctx.__sentry_transaction = transaction; + + // We put the transaction on the scope so users can attach children to it + Sentry.getCurrentHub().configureScope((scope) => { + scope.setSpan(transaction); + }); + + ctx.res.on("finish", () => { + // Push \`transaction.finish\` to the next event loop so open spans have a chance to finish before the transaction closes + setImmediate(() => { + // If you're using koa router, set the matched route as transaction name + if (ctx._matchedRoute) { + const mountPath = ctx.mountPath || ""; + transaction.setName(\`\${reqMethod} \${mountPath}\${ctx._matchedRoute}\`); + } + transaction.setHttpStatus(ctx.status); + transaction.finish(); + }); + }); + + await next(); +}; - app.listen(3000); +app.use(requestHandler); +app.use(tracingMiddleWare);` + : '' + } + +// Send errors to Sentry +app.on("error", (err, ctx) => { + Sentry.withScope((scope) => { + scope.addEventProcessor((event) => { + return Sentry.addRequestDataToEvent(event, ctx.request); + }); + Sentry.captureException(err); + }); +}); + +app.listen(3000); `, }, ], @@ -165,24 +168,47 @@ npm install --save @sentry/node @sentry/utils }, ]; -export function GettingStartedWithKoa({dsn, newOrg, platformKey}: ModuleProps) { - let sentryInitContent: string[] = [`dsn: "${dsn}",`]; - - const integrations = [...performanceIntegrations]; - const otherConfigs = [performanceOtherConfig]; - - if (integrations.length > 0) { - sentryInitContent = sentryInitContent.concat('integrations: [', integrations, '],'); - } - - if (otherConfigs.length > 0) { - sentryInitContent = sentryInitContent.concat(otherConfigs); - } +export function GettingStartedWithKoa({ + dsn, + newOrg, + platformKey, + activeProductSelection = [], +}: ModuleProps) { + const productSelection = getProductSelectionMap(activeProductSelection); + + const additionalPackages = productSelection['performance-monitoring'] + ? ['@sentry/utils'] + : []; + const installSnippet = getInstallSnippet({productSelection, additionalPackages}); + let imports = getDefaultNodeImports({productSelection}); + imports = imports.concat([ + 'import { stripUrlQueryAndFragment } from "@sentry/utils";', + 'import Koa from "koa";', + ]); + + const integrations = [ + ...(productSelection['performance-monitoring'] ? performanceIntegrations : []), + ...getProductIntegrations({productSelection}), + ]; + + const integrationParam = + integrations.length > 0 + ? `integrations: [\n${joinWithIndentation(integrations)}\n],` + : null; + + const initContent = joinWithIndentation([ + ...getDefaultInitParams({dsn}), + ...(integrationParam ? [integrationParam] : []), + ...getProductInitParams({productSelection}), + ]); return ( Date: Thu, 31 Aug 2023 15:08:40 +0200 Subject: [PATCH 3/3] fix(onboarding): Fix tests --- static/app/gettingStartedDocs/node/express.spec.tsx | 7 ++++++- static/app/gettingStartedDocs/node/koa.spec.tsx | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/static/app/gettingStartedDocs/node/express.spec.tsx b/static/app/gettingStartedDocs/node/express.spec.tsx index ee5754ede820..700cae7666a8 100644 --- a/static/app/gettingStartedDocs/node/express.spec.tsx +++ b/static/app/gettingStartedDocs/node/express.spec.tsx @@ -9,7 +9,12 @@ describe('GettingStartedWithExpress', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({ + installSnippet: 'test-install-snippet', + importContent: 'test-import-content', + initContent: 'test-init-content', + hasPerformanceMonitoring: true, + })) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/node/koa.spec.tsx b/static/app/gettingStartedDocs/node/koa.spec.tsx index e235b0cd4750..d49afc27cdbc 100644 --- a/static/app/gettingStartedDocs/node/koa.spec.tsx +++ b/static/app/gettingStartedDocs/node/koa.spec.tsx @@ -9,7 +9,12 @@ describe('GettingStartedWithKoa', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({ + installSnippet: 'test-install-snippet', + importContent: 'test-import-content', + initContent: 'test-init-content', + hasPerformanceMonitoring: true, + })) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument();