diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/directive-skipAuth/code.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/directive-skipAuth/code.js new file mode 100644 index 000000000000..e85b94ae8b89 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/directive-skipAuth/code.js @@ -0,0 +1,16 @@ +import gql from 'graphql-tag' + +import { createValidatorDirective } from '@redwoodjs/graphql-server' + +export const schema = gql` + """ + Use to skip authentication checks and allow public access. + """ + directive @skipAuth on FIELD_DEFINITION +` + +const skipAuth = createValidatorDirective(schema, () => { + return +}) + +export default skipAuth diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/directive-skipAuth/output.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/directive-skipAuth/output.js new file mode 100644 index 000000000000..40d4c728bfbd --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/directive-skipAuth/output.js @@ -0,0 +1,13 @@ +import { trace as RW_OTEL_WRAPPER_TRACE } from '@opentelemetry/api' +import gql from 'graphql-tag' +import { createValidatorDirective } from '@redwoodjs/graphql-server' +export const schema = gql` + """ + Use to skip authentication checks and allow public access. + """ + directive @skipAuth on FIELD_DEFINITION +` +const skipAuth = createValidatorDirective(schema, () => { + return +}) +export default skipAuth \ No newline at end of file diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/function-auth/code.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/function-auth/code.js new file mode 100644 index 000000000000..b3494cb4d877 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/function-auth/code.js @@ -0,0 +1,102 @@ +import { DbAuthHandler, DbAuthHandlerOptions } from '@redwoodjs/auth-dbauth-api' + +import { db } from 'src/lib/db' + +export const handler = async ( + event, + context +) => { + const forgotPasswordOptions = { + handler: (user) => { + return user + }, + + expires: 60 * 60 * 24, + + errors: { + usernameNotFound: 'Username not found', + usernameRequired: 'Username is required', + }, + } + + const loginOptions = { + handler: (user) => { + return user + }, + + errors: { + usernameOrPasswordMissing: 'Both username and password are required', + usernameNotFound: 'Username ${username} not found', + incorrectPassword: 'Incorrect password for ${username}', + }, + + expires: 60 * 60 * 24 * 365 * 10, + } + + const resetPasswordOptions = { + handler: (_user) => { + return true + }, + + allowReusedPassword: true, + + errors: { + resetTokenExpired: 'resetToken is expired', + resetTokenInvalid: 'resetToken is invalid', + resetTokenRequired: 'resetToken is required', + reusedPassword: 'Must choose a new password', + }, + } + + const signupOptions = { + handler: ({ username, hashedPassword, salt, userAttributes }) => { + return db.user.create({ + data: { + email: username, + hashedPassword: hashedPassword, + salt: salt, + fullName: userAttributes['full-name'], + }, + }) + }, + + passwordValidation: (_password) => { + return true + }, + + errors: { + fieldMissing: '${field} is required', + usernameTaken: 'Username `${username}` already in use', + }, + } + + const authHandler = new DbAuthHandler(event, context, { + db: db, + + authModelAccessor: 'user', + + authFields: { + id: 'id', + username: 'email', + hashedPassword: 'hashedPassword', + salt: 'salt', + resetToken: 'resetToken', + resetTokenExpiresAt: 'resetTokenExpiresAt', + }, + + cookie: { + HttpOnly: true, + Path: '/', + SameSite: 'Strict', + Secure: process.env.NODE_ENV !== 'development', + + }, + + forgotPassword: forgotPasswordOptions, + login: loginOptions, + resetPassword: resetPasswordOptions, + signup: signupOptions, + }) + + return await authHandler.invoke() +} diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/function-auth/output.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/function-auth/output.js new file mode 100644 index 000000000000..c2e265b1563b --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/function-auth/output.js @@ -0,0 +1,105 @@ +import { trace as RW_OTEL_WRAPPER_TRACE } from '@opentelemetry/api' +import { DbAuthHandler, DbAuthHandlerOptions } from '@redwoodjs/auth-dbauth-api' +import { db } from 'src/lib/db' +export const handler = async (event, context) => { + const __handler = async (event, context) => { + const forgotPasswordOptions = { + handler: (user) => { + return user + }, + expires: 60 * 60 * 24, + errors: { + usernameNotFound: 'Username not found', + usernameRequired: 'Username is required', + }, + } + const loginOptions = { + handler: (user) => { + return user + }, + errors: { + usernameOrPasswordMissing: 'Both username and password are required', + usernameNotFound: 'Username ${username} not found', + incorrectPassword: 'Incorrect password for ${username}', + }, + expires: 60 * 60 * 24 * 365 * 10, + } + const resetPasswordOptions = { + handler: (_user) => { + return true + }, + allowReusedPassword: true, + errors: { + resetTokenExpired: 'resetToken is expired', + resetTokenInvalid: 'resetToken is invalid', + resetTokenRequired: 'resetToken is required', + reusedPassword: 'Must choose a new password', + }, + } + const signupOptions = { + handler: ({ username, hashedPassword, salt, userAttributes }) => { + return db.user.create({ + data: { + email: username, + hashedPassword: hashedPassword, + salt: salt, + fullName: userAttributes['full-name'], + }, + }) + }, + passwordValidation: (_password) => { + return true + }, + errors: { + fieldMissing: '${field} is required', + usernameTaken: 'Username `${username}` already in use', + }, + } + const authHandler = new DbAuthHandler(event, context, { + db: db, + authModelAccessor: 'user', + authFields: { + id: 'id', + username: 'email', + hashedPassword: 'hashedPassword', + salt: 'salt', + resetToken: 'resetToken', + resetTokenExpiresAt: 'resetTokenExpiresAt', + }, + cookie: { + HttpOnly: true, + Path: '/', + SameSite: 'Strict', + Secure: process.env.NODE_ENV !== 'development', + }, + forgotPassword: forgotPasswordOptions, + login: loginOptions, + resetPassword: resetPasswordOptions, + signup: signupOptions, + }) + return await authHandler.invoke() + } + const RW_OTEL_WRAPPER_TRACER = RW_OTEL_WRAPPER_TRACE.getTracer('redwoodjs') + const RW_OTEL_WRAPPER_RESULT = await RW_OTEL_WRAPPER_TRACER.startActiveSpan( + 'redwoodjs:api:__MOCKED_API_FOLDER__:handler', + async (span) => { + span.setAttribute('code.function', 'handler') + span.setAttribute('code.filepath', '__MOCKED_FILENAME__') + try { + const RW_OTEL_WRAPPER_INNER_RESULT = await __handler(event, context) + span.end() + return RW_OTEL_WRAPPER_INNER_RESULT + } catch (error) { + span.recordException(error) + span.setStatus({ + code: 2, + message: + error?.message?.split('\n')[0] ?? error?.toString()?.split('\n')[0], + }) + span.end() + throw error + } + } + ) + return RW_OTEL_WRAPPER_RESULT +} \ No newline at end of file diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/function-graphql/code.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/function-graphql/code.js new file mode 100644 index 000000000000..5d8db6ab8f2a --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/function-graphql/code.js @@ -0,0 +1,23 @@ +import { authDecoder } from '@redwoodjs/auth-dbauth-api' +import { createGraphQLHandler } from '@redwoodjs/graphql-server' + +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' + +import { getCurrentUser } from 'src/lib/auth' +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' + +export const handler = createGraphQLHandler({ + authDecoder, + getCurrentUser, + loggerConfig: { logger, options: {} }, + directives, + sdls, + services, + onException: () => { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, +}) diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/function-graphql/output.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/function-graphql/output.js new file mode 100644 index 000000000000..873ed281570b --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/function-graphql/output.js @@ -0,0 +1,24 @@ +import { trace as RW_OTEL_WRAPPER_TRACE } from '@opentelemetry/api' +import { authDecoder } from '@redwoodjs/auth-dbauth-api' +import { createGraphQLHandler } from '@redwoodjs/graphql-server' +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' +import { getCurrentUser } from 'src/lib/auth' +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' +export const handler = createGraphQLHandler({ + authDecoder, + getCurrentUser, + loggerConfig: { + logger, + options: {}, + }, + directives, + sdls, + services, + onException: () => { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, +}) \ No newline at end of file diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/lib-auth/code.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/lib-auth/code.js new file mode 100644 index 000000000000..a268c008e449 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/lib-auth/code.js @@ -0,0 +1,61 @@ +import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server' + +import { db } from './db' + +export const getCurrentUser = async (session) => { + if (!session || typeof session.id !== 'number') { + throw new Error('Invalid session') + } + + return await db.user.findUnique({ + where: { id: session.id }, + select: { id: true, roles: true, email: true }, + }) +} + +export const isAuthenticated = () => { + return !!context.currentUser +} + +export const hasRole = (roles) => { + if (!isAuthenticated()) { + return false + } + + const currentUserRoles = context.currentUser?.roles + + if (typeof roles === 'string') { + if (typeof currentUserRoles === 'string') { + // roles to check is a string, currentUser.roles is a string + return currentUserRoles === roles + } else if (Array.isArray(currentUserRoles)) { + // roles to check is a string, currentUser.roles is an array + return currentUserRoles?.some((allowedRole) => roles === allowedRole) + } + } + + if (Array.isArray(roles)) { + if (Array.isArray(currentUserRoles)) { + // roles to check is an array, currentUser.roles is an array + return currentUserRoles?.some((allowedRole) => + roles.includes(allowedRole) + ) + } else if (typeof currentUserRoles === 'string') { + // roles to check is an array, currentUser.roles is a string + return roles.some((allowedRole) => currentUserRoles === allowedRole) + } + } + + // roles not found + return false +} + +export const requireAuth = ({ roles } = {}) => { + if (!isAuthenticated()) { + throw new AuthenticationError("You don't have permission to do that.") + } + + if (roles && !hasRole(roles)) { + throw new ForbiddenError("You don't have access to do that.") + } +} diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/lib-auth/output.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/lib-auth/output.js new file mode 100644 index 000000000000..893341755924 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/lib-auth/output.js @@ -0,0 +1,160 @@ +import { trace as RW_OTEL_WRAPPER_TRACE } from '@opentelemetry/api' +import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server' +import { db } from './db' +export const getCurrentUser = async (session) => { + const __getCurrentUser = async (session) => { + if (!session || typeof session.id !== 'number') { + throw new Error('Invalid session') + } + return await db.user.findUnique({ + where: { + id: session.id, + }, + select: { + id: true, + roles: true, + email: true, + }, + }) + } + const RW_OTEL_WRAPPER_TRACER = RW_OTEL_WRAPPER_TRACE.getTracer('redwoodjs') + const RW_OTEL_WRAPPER_RESULT = await RW_OTEL_WRAPPER_TRACER.startActiveSpan( + 'redwoodjs:api:__MOCKED_API_FOLDER__:getCurrentUser', + async (span) => { + span.setAttribute('code.function', 'getCurrentUser') + span.setAttribute('code.filepath', '__MOCKED_FILENAME__') + try { + const RW_OTEL_WRAPPER_INNER_RESULT = await __getCurrentUser(session) + span.end() + return RW_OTEL_WRAPPER_INNER_RESULT + } catch (error) { + span.recordException(error) + span.setStatus({ + code: 2, + message: + error?.message?.split('\n')[0] ?? error?.toString()?.split('\n')[0], + }) + span.end() + throw error + } + } + ) + return RW_OTEL_WRAPPER_RESULT +} +export const isAuthenticated = () => { + const __isAuthenticated = () => { + return !!context.currentUser + } + const RW_OTEL_WRAPPER_TRACER = RW_OTEL_WRAPPER_TRACE.getTracer('redwoodjs') + const RW_OTEL_WRAPPER_RESULT = RW_OTEL_WRAPPER_TRACER.startActiveSpan( + 'redwoodjs:api:__MOCKED_API_FOLDER__:isAuthenticated', + (span) => { + span.setAttribute('code.function', 'isAuthenticated') + span.setAttribute('code.filepath', '__MOCKED_FILENAME__') + try { + const RW_OTEL_WRAPPER_INNER_RESULT = __isAuthenticated() + span.end() + return RW_OTEL_WRAPPER_INNER_RESULT + } catch (error) { + span.recordException(error) + span.setStatus({ + code: 2, + message: + error?.message?.split('\n')[0] ?? error?.toString()?.split('\n')[0], + }) + span.end() + throw error + } + } + ) + return RW_OTEL_WRAPPER_RESULT +} +export const hasRole = (roles) => { + const __hasRole = (roles) => { + if (!isAuthenticated()) { + return false + } + const currentUserRoles = context.currentUser?.roles + if (typeof roles === 'string') { + if (typeof currentUserRoles === 'string') { + // roles to check is a string, currentUser.roles is a string + return currentUserRoles === roles + } else if (Array.isArray(currentUserRoles)) { + // roles to check is a string, currentUser.roles is an array + return currentUserRoles?.some((allowedRole) => roles === allowedRole) + } + } + if (Array.isArray(roles)) { + if (Array.isArray(currentUserRoles)) { + // roles to check is an array, currentUser.roles is an array + return currentUserRoles?.some((allowedRole) => + roles.includes(allowedRole) + ) + } else if (typeof currentUserRoles === 'string') { + // roles to check is an array, currentUser.roles is a string + return roles.some((allowedRole) => currentUserRoles === allowedRole) + } + } + + // roles not found + return false + } + const RW_OTEL_WRAPPER_TRACER = RW_OTEL_WRAPPER_TRACE.getTracer('redwoodjs') + const RW_OTEL_WRAPPER_RESULT = RW_OTEL_WRAPPER_TRACER.startActiveSpan( + 'redwoodjs:api:__MOCKED_API_FOLDER__:hasRole', + (span) => { + span.setAttribute('code.function', 'hasRole') + span.setAttribute('code.filepath', '__MOCKED_FILENAME__') + try { + const RW_OTEL_WRAPPER_INNER_RESULT = __hasRole(roles) + span.end() + return RW_OTEL_WRAPPER_INNER_RESULT + } catch (error) { + span.recordException(error) + span.setStatus({ + code: 2, + message: + error?.message?.split('\n')[0] ?? error?.toString()?.split('\n')[0], + }) + span.end() + throw error + } + } + ) + return RW_OTEL_WRAPPER_RESULT +} +export const requireAuth = ({ roles } = {}) => { + const __requireAuth = ({ roles } = {}) => { + if (!isAuthenticated()) { + throw new AuthenticationError("You don't have permission to do that.") + } + if (roles && !hasRole(roles)) { + throw new ForbiddenError("You don't have access to do that.") + } + } + const RW_OTEL_WRAPPER_TRACER = RW_OTEL_WRAPPER_TRACE.getTracer('redwoodjs') + const RW_OTEL_WRAPPER_RESULT = RW_OTEL_WRAPPER_TRACER.startActiveSpan( + 'redwoodjs:api:__MOCKED_API_FOLDER__:requireAuth', + (span) => { + span.setAttribute('code.function', 'requireAuth') + span.setAttribute('code.filepath', '__MOCKED_FILENAME__') + try { + const RW_OTEL_WRAPPER_INNER_RESULT = __requireAuth({ + roles, + }) + span.end() + return RW_OTEL_WRAPPER_INNER_RESULT + } catch (error) { + span.recordException(error) + span.setStatus({ + code: 2, + message: + error?.message?.split('\n')[0] ?? error?.toString()?.split('\n')[0], + }) + span.end() + throw error + } + } + ) + return RW_OTEL_WRAPPER_RESULT +} \ No newline at end of file diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/lib-db/code.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/lib-db/code.js new file mode 100644 index 000000000000..63a4c976ab1c --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/lib-db/code.js @@ -0,0 +1,18 @@ +import { PrismaClient } from '@prisma/client' + +import { emitLogLevels, handlePrismaLogging } from '@redwoodjs/api/logger' + +import { logger } from './logger' + +/* + * Instance of the Prisma Client + */ +export const db = new PrismaClient({ + log: emitLogLevels(['info', 'warn', 'error']), +}) + +handlePrismaLogging({ + db, + logger, + logLevels: ['info', 'warn', 'error'], +}) diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/lib-db/output.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/lib-db/output.js new file mode 100644 index 000000000000..e70d8a743d2e --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/lib-db/output.js @@ -0,0 +1,16 @@ +import { trace as RW_OTEL_WRAPPER_TRACE } from '@opentelemetry/api' +import { PrismaClient } from '@prisma/client' +import { emitLogLevels, handlePrismaLogging } from '@redwoodjs/api/logger' +import { logger } from './logger' + +/* + * Instance of the Prisma Client + */ +export const db = new PrismaClient({ + log: emitLogLevels(['info', 'warn', 'error']), +}) +handlePrismaLogging({ + db, + logger, + logLevels: ['info', 'warn', 'error'], +}) \ No newline at end of file diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-basic/code.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-basic/code.js new file mode 100644 index 000000000000..6bdecbe8123b --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-basic/code.js @@ -0,0 +1,35 @@ +import { db } from 'src/lib/db' + +export const contacts = () => { + return db.contact.findMany() +} + +export const contact = ({ id }) => { + return db.contact.findUnique({ + where: { id }, + }) +} + +export const createContact = ({ + input, +}) => { + return db.contact.create({ + data: input, + }) +} + +export const updateContact = ({ + id, + input, +}) => { + return db.contact.update({ + data: input, + where: { id }, + }) +} + +export const deleteContact = ({ id }) => { + return db.contact.delete({ + where: { id }, + }) +} diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-basic/output.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-basic/output.js new file mode 100644 index 000000000000..bede484d4d4b --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-basic/output.js @@ -0,0 +1,166 @@ +import { trace as RW_OTEL_WRAPPER_TRACE } from '@opentelemetry/api' +import { db } from 'src/lib/db' +export const contacts = () => { + const __contacts = () => { + return db.contact.findMany() + } + const RW_OTEL_WRAPPER_TRACER = RW_OTEL_WRAPPER_TRACE.getTracer('redwoodjs') + const RW_OTEL_WRAPPER_RESULT = RW_OTEL_WRAPPER_TRACER.startActiveSpan( + 'redwoodjs:api:__MOCKED_API_FOLDER__:contacts', + (span) => { + span.setAttribute('code.function', 'contacts') + span.setAttribute('code.filepath', '__MOCKED_FILENAME__') + try { + const RW_OTEL_WRAPPER_INNER_RESULT = __contacts() + span.end() + return RW_OTEL_WRAPPER_INNER_RESULT + } catch (error) { + span.recordException(error) + span.setStatus({ + code: 2, + message: + error?.message?.split('\n')[0] ?? error?.toString()?.split('\n')[0], + }) + span.end() + throw error + } + } + ) + return RW_OTEL_WRAPPER_RESULT +} +export const contact = ({ id }) => { + const __contact = ({ id }) => { + return db.contact.findUnique({ + where: { + id, + }, + }) + } + const RW_OTEL_WRAPPER_TRACER = RW_OTEL_WRAPPER_TRACE.getTracer('redwoodjs') + const RW_OTEL_WRAPPER_RESULT = RW_OTEL_WRAPPER_TRACER.startActiveSpan( + 'redwoodjs:api:__MOCKED_API_FOLDER__:contact', + (span) => { + span.setAttribute('code.function', 'contact') + span.setAttribute('code.filepath', '__MOCKED_FILENAME__') + try { + const RW_OTEL_WRAPPER_INNER_RESULT = __contact({ + id, + }) + span.end() + return RW_OTEL_WRAPPER_INNER_RESULT + } catch (error) { + span.recordException(error) + span.setStatus({ + code: 2, + message: + error?.message?.split('\n')[0] ?? error?.toString()?.split('\n')[0], + }) + span.end() + throw error + } + } + ) + return RW_OTEL_WRAPPER_RESULT +} +export const createContact = ({ input }) => { + const __createContact = ({ input }) => { + return db.contact.create({ + data: input, + }) + } + const RW_OTEL_WRAPPER_TRACER = RW_OTEL_WRAPPER_TRACE.getTracer('redwoodjs') + const RW_OTEL_WRAPPER_RESULT = RW_OTEL_WRAPPER_TRACER.startActiveSpan( + 'redwoodjs:api:__MOCKED_API_FOLDER__:createContact', + (span) => { + span.setAttribute('code.function', 'createContact') + span.setAttribute('code.filepath', '__MOCKED_FILENAME__') + try { + const RW_OTEL_WRAPPER_INNER_RESULT = __createContact({ + input, + }) + span.end() + return RW_OTEL_WRAPPER_INNER_RESULT + } catch (error) { + span.recordException(error) + span.setStatus({ + code: 2, + message: + error?.message?.split('\n')[0] ?? error?.toString()?.split('\n')[0], + }) + span.end() + throw error + } + } + ) + return RW_OTEL_WRAPPER_RESULT +} +export const updateContact = ({ id, input }) => { + const __updateContact = ({ id, input }) => { + return db.contact.update({ + data: input, + where: { + id, + }, + }) + } + const RW_OTEL_WRAPPER_TRACER = RW_OTEL_WRAPPER_TRACE.getTracer('redwoodjs') + const RW_OTEL_WRAPPER_RESULT = RW_OTEL_WRAPPER_TRACER.startActiveSpan( + 'redwoodjs:api:__MOCKED_API_FOLDER__:updateContact', + (span) => { + span.setAttribute('code.function', 'updateContact') + span.setAttribute('code.filepath', '__MOCKED_FILENAME__') + try { + const RW_OTEL_WRAPPER_INNER_RESULT = __updateContact({ + id, + input, + }) + span.end() + return RW_OTEL_WRAPPER_INNER_RESULT + } catch (error) { + span.recordException(error) + span.setStatus({ + code: 2, + message: + error?.message?.split('\n')[0] ?? error?.toString()?.split('\n')[0], + }) + span.end() + throw error + } + } + ) + return RW_OTEL_WRAPPER_RESULT +} +export const deleteContact = ({ id }) => { + const __deleteContact = ({ id }) => { + return db.contact.delete({ + where: { + id, + }, + }) + } + const RW_OTEL_WRAPPER_TRACER = RW_OTEL_WRAPPER_TRACE.getTracer('redwoodjs') + const RW_OTEL_WRAPPER_RESULT = RW_OTEL_WRAPPER_TRACER.startActiveSpan( + 'redwoodjs:api:__MOCKED_API_FOLDER__:deleteContact', + (span) => { + span.setAttribute('code.function', 'deleteContact') + span.setAttribute('code.filepath', '__MOCKED_FILENAME__') + try { + const RW_OTEL_WRAPPER_INNER_RESULT = __deleteContact({ + id, + }) + span.end() + return RW_OTEL_WRAPPER_INNER_RESULT + } catch (error) { + span.recordException(error) + span.setStatus({ + code: 2, + message: + error?.message?.split('\n')[0] ?? error?.toString()?.split('\n')[0], + }) + span.end() + throw error + } + } + ) + return RW_OTEL_WRAPPER_RESULT +} \ No newline at end of file diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-custom/code.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-custom/code.js new file mode 100644 index 000000000000..5d01ea5c350f --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-custom/code.js @@ -0,0 +1,25 @@ +// This example function has default values in the function signature +export const withDefaultValues = async ({ + id, + process = true, + output = [], + backup = () => ('backup'), +}) => { + if (process) { + output.push(backup()) + } + return `${id}: ${output.join('\t')}` +} + +// This example function has a different default value definition in the function signature +export const withDefaultValuesTwo = async (args = { + id, + process: true, + output: [], + backup: () => ('backup'), +}) => { + if (args.process) { + args.output.push(args.backup()) + } + return `${args.id}: ${args.output.join('\t')}` +} diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-custom/output.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-custom/output.js new file mode 100644 index 000000000000..fdf834704bb9 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-custom/output.js @@ -0,0 +1,95 @@ +import { trace as RW_OTEL_WRAPPER_TRACE } from '@opentelemetry/api' +// This example function has default values in the function signature +export const withDefaultValues = async ({ + id, + process = true, + output = [], + backup = () => 'backup', +}) => { + const __withDefaultValues = async ({ + id, + process = true, + output = [], + backup = () => 'backup', + }) => { + if (process) { + output.push(backup()) + } + return `${id}: ${output.join('\t')}` + } + const RW_OTEL_WRAPPER_TRACER = RW_OTEL_WRAPPER_TRACE.getTracer('redwoodjs') + const RW_OTEL_WRAPPER_RESULT = await RW_OTEL_WRAPPER_TRACER.startActiveSpan( + 'redwoodjs:api:__MOCKED_API_FOLDER__:withDefaultValues', + async (span) => { + span.setAttribute('code.function', 'withDefaultValues') + span.setAttribute('code.filepath', '__MOCKED_FILENAME__') + try { + const RW_OTEL_WRAPPER_INNER_RESULT = await __withDefaultValues({ + id, + process: process, + output: output, + backup: backup, + }) + span.end() + return RW_OTEL_WRAPPER_INNER_RESULT + } catch (error) { + span.recordException(error) + span.setStatus({ + code: 2, + message: + error?.message?.split('\n')[0] ?? error?.toString()?.split('\n')[0], + }) + span.end() + throw error + } + } + ) + return RW_OTEL_WRAPPER_RESULT +} + +// This example function has a different default value definition in the function signature +export const withDefaultValuesTwo = async ( + args = { + id, + process: true, + output: [], + backup: () => 'backup', + } +) => { + const __withDefaultValuesTwo = async ( + args = { + id, + process: true, + output: [], + backup: () => 'backup', + } + ) => { + if (args.process) { + args.output.push(args.backup()) + } + return `${args.id}: ${args.output.join('\t')}` + } + const RW_OTEL_WRAPPER_TRACER = RW_OTEL_WRAPPER_TRACE.getTracer('redwoodjs') + const RW_OTEL_WRAPPER_RESULT = await RW_OTEL_WRAPPER_TRACER.startActiveSpan( + 'redwoodjs:api:__MOCKED_API_FOLDER__:withDefaultValuesTwo', + async (span) => { + span.setAttribute('code.function', 'withDefaultValuesTwo') + span.setAttribute('code.filepath', '__MOCKED_FILENAME__') + try { + const RW_OTEL_WRAPPER_INNER_RESULT = await __withDefaultValuesTwo(args) + span.end() + return RW_OTEL_WRAPPER_INNER_RESULT + } catch (error) { + span.recordException(error) + span.setStatus({ + code: 2, + message: + error?.message?.split('\n')[0] ?? error?.toString()?.split('\n')[0], + }) + span.end() + throw error + } + } + ) + return RW_OTEL_WRAPPER_RESULT +} \ No newline at end of file diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-instrumented/code.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-instrumented/code.js new file mode 100644 index 000000000000..210393e41988 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-instrumented/code.js @@ -0,0 +1,22 @@ +import opentelemetry from '@opentelemetry/api' + +import { db } from 'src/lib/db' + +export const updateContact = ({ + id, + input, +} = { + id: 1, + input: { + name: 'R. Edwoods', + }, + }) => { + return opentelemetry.trace.getTracer('service').startActiveSpan('updateContact', async (span) => { + const data = await db.contact.update({ + data: input, + where: { id }, + }) + span.end() + return data + }) +} diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-instrumented/output.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-instrumented/output.js new file mode 100644 index 000000000000..cf325a44c74b --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/otel-wrapping/service-instrumented/output.js @@ -0,0 +1,59 @@ +import { trace as RW_OTEL_WRAPPER_TRACE } from '@opentelemetry/api' +import opentelemetry from '@opentelemetry/api' +import { db } from 'src/lib/db' +export const updateContact = ( + { id, input } = { + id: 1, + input: { + name: 'R. Edwoods', + }, + } +) => { + const __updateContact = ( + { id, input } = { + id: 1, + input: { + name: 'R. Edwoods', + }, + } + ) => { + return opentelemetry.trace + .getTracer('service') + .startActiveSpan('updateContact', async (span) => { + const data = await db.contact.update({ + data: input, + where: { + id, + }, + }) + span.end() + return data + }) + } + const RW_OTEL_WRAPPER_TRACER = RW_OTEL_WRAPPER_TRACE.getTracer('redwoodjs') + const RW_OTEL_WRAPPER_RESULT = RW_OTEL_WRAPPER_TRACER.startActiveSpan( + 'redwoodjs:api:__MOCKED_API_FOLDER__:updateContact', + (span) => { + span.setAttribute('code.function', 'updateContact') + span.setAttribute('code.filepath', '__MOCKED_FILENAME__') + try { + const RW_OTEL_WRAPPER_INNER_RESULT = __updateContact({ + id, + input, + }) + span.end() + return RW_OTEL_WRAPPER_INNER_RESULT + } catch (error) { + span.recordException(error) + span.setStatus({ + code: 2, + message: + error?.message?.split('\n')[0] ?? error?.toString()?.split('\n')[0], + }) + span.end() + throw error + } + } + ) + return RW_OTEL_WRAPPER_RESULT +} \ No newline at end of file diff --git a/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-otel-wrapping.test.ts b/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-otel-wrapping.test.ts new file mode 100644 index 000000000000..37ec1d2d5394 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-otel-wrapping.test.ts @@ -0,0 +1,19 @@ +import path from 'path' + +import pluginTester from 'babel-plugin-tester' + +import redwoodOtelWrappingPlugin from '../babel-plugin-redwood-otel-wrapping' + +jest.mock('@redwoodjs/project-config', () => { + return { + getBaseDirFromFile: () => { + return '' + }, + } +}) + +pluginTester({ + plugin: redwoodOtelWrappingPlugin, + pluginName: 'babel-plugin-redwood-otel-wrapping', + fixtures: path.join(__dirname, '__fixtures__/otel-wrapping'), +}) diff --git a/packages/babel-config/src/plugins/babel-plugin-redwood-otel-wrapping.ts b/packages/babel-config/src/plugins/babel-plugin-redwood-otel-wrapping.ts index 03befb374bfe..7b1ab73644aa 100644 --- a/packages/babel-config/src/plugins/babel-plugin-redwood-otel-wrapping.ts +++ b/packages/babel-config/src/plugins/babel-plugin-redwood-otel-wrapping.ts @@ -1,311 +1,362 @@ import * as nodejsPath from 'path' -import type { PluginObj, types } from '@babel/core' +import type { NodePath, PluginObj, PluginPass, types } from '@babel/core' import { getBaseDirFromFile } from '@redwoodjs/project-config' -// This wraps user code within opentelemetry spans to provide greater ease in trace analysis. +// This wraps user code within opentelemetry spans to provide automatic tracing in your redwood API. + +function addOpenTelemetryImport( + path: NodePath, + t: typeof types +) { + // We need to have access to the `trace` from `@opentelemetry/api` in order to add the + // automatic instrumentation. We will import it and alias it to something highly specific + // to avoid any potential naming conflicts with user code. + path.node.body.unshift( + t.importDeclaration( + [ + t.importSpecifier( + t.identifier('RW_OTEL_WRAPPER_TRACE'), + t.identifier('trace') + ), + ], + t.stringLiteral('@opentelemetry/api') + ) + ) +} + +function getRedwoodPaths(state: PluginPass): { + filename: string | null | undefined + apiFolder: string +} { + // NOTE: Unable to get 'babel-plugin-tester' to mock the filename so we have specific + // testing logic here. Not ideal but it works for now. + if (process.env.NODE_ENV === 'test') { + return { + filename: '__MOCKED_FILENAME__', + apiFolder: '__MOCKED_API_FOLDER__', + } + } + + const filename = state.file.opts.filename + const filenameOffset = filename + ? getBaseDirFromFile(filename).length + 9 // 9 is the length of '/api/src/' + : 0 + const apiFolder = filename + ? filename.substring( + filenameOffset, + filename.substring(filenameOffset).indexOf(nodejsPath.sep) + + filenameOffset + ) + : '?' -export default function ({ types: t }: { types: typeof types }): PluginObj { return { - name: 'babel-plugin-redwood-otel-wrapping', - visitor: { - Program(path) { - // Only import if it isn't already imported in the way we need it - // TODO: Check for ImportNamespaceSpecifier like "import * as opentelemetry from '@opentelemetry/api'" - // TODO: Consider just checking for the import name "opentelemetry" and don't consider the source - const importDeclarations = path.node.body.filter( - (node) => node.type === 'ImportDeclaration' - ) as types.ImportDeclaration[] - const requiredOpenTelemetryImportExists = importDeclarations.some( - (importDeclaration) => { - if (importDeclaration.source.value !== '@opentelemetry/api') { - return false - } - if ( - importDeclaration.specifiers[0].type !== 'ImportDefaultSpecifier' - ) { - return false - } - if ( - importDeclaration.specifiers[0].local.name === 'opentelemetry' - ) { - return true + filename, + apiFolder, + } +} + +function wrapExportNamedDeclaration( + path: NodePath, + state: PluginPass, + t: typeof types +) { + const declaration = path.node.declaration + const declarationIsSupported = + declaration != null && + declaration.type === 'VariableDeclaration' && + declaration.declarations[0].init?.type === 'ArrowFunctionExpression' + if (!declarationIsSupported) { + return + } + + const originalFunction = declaration.declarations[0] + .init as types.ArrowFunctionExpression + if (!originalFunction) { + return + } + + const originalFunctionName = + declaration.declarations[0].id.type === 'Identifier' + ? declaration.declarations[0].id.name + : '?' + const wrappedFunctionName = `__${ + originalFunctionName === '?' + ? 'RW_OTEL_WRAPPER_UNKNOWN_FUNCTION' + : originalFunctionName + }` + + const originalFunctionArgumentsWithoutDefaults: ( + | types.ArgumentPlaceholder + | types.JSXNamespacedName + | types.SpreadElement + | types.Expression + )[] = [] + for (const param of originalFunction.params) { + if (param.type === 'Identifier') { + originalFunctionArgumentsWithoutDefaults.push(param) + continue + } + + if (param.type === 'ObjectPattern') { + const objectProperties = param.properties.filter( + (p) => p.type === 'ObjectProperty' + ) as types.ObjectProperty[] + originalFunctionArgumentsWithoutDefaults.push( + t.objectExpression( + objectProperties.map((p) => { + if (p.value.type === 'AssignmentPattern') { + return t.objectProperty(p.key, p.value.left) } - return false - } + return p + }) ) - if (!requiredOpenTelemetryImportExists) { - path.node.body.unshift( - t.importDeclaration( - [t.importDefaultSpecifier(t.identifier('opentelemetry'))], - t.stringLiteral('@opentelemetry/api') - ) + ) + + continue + } + + if (param.type === 'AssignmentPattern') { + if (param.left.type === 'Identifier') { + originalFunctionArgumentsWithoutDefaults.push(param.left) + } else if (param.left.type === 'ObjectPattern') { + const objectProperties = param.left.properties.filter( + (p) => p.type === 'ObjectProperty' + ) as types.ObjectProperty[] + originalFunctionArgumentsWithoutDefaults.push( + t.objectExpression( + objectProperties.map((p) => { + if (p.value.type === 'AssignmentPattern') { + return t.objectProperty(p.key, p.value.left) + } + return p + }) ) - } - }, - ExportNamedDeclaration(path, state) { - if ( - path.node.declaration?.type === 'VariableDeclaration' && - path.node.declaration.declarations[0].init?.type === - 'ArrowFunctionExpression' - ) { - const originalFunc = path.node.declaration.declarations[0].init - const originalFuncId = - path.node.declaration.declarations[0].id.type === 'Identifier' - ? path.node.declaration.declarations[0].id.name - : '?' + ) + } else { + // TODO: Implement others, bail out for now + return + } + } - const originalFuncArguments: ( - | types.ArgumentPlaceholder - | types.JSXNamespacedName - | types.SpreadElement - | types.Expression - )[] = [] - for (const param of originalFunc.params) { - switch (param.type) { - case 'ArrayPattern': - // TODO: Implement me - break - case 'AssignmentPattern': - // TODO: Implement me - break - case 'Identifier': - originalFuncArguments.push(param) - // TODO: Implement me - break - case 'ObjectPattern': - // TODO: Is this correct? - originalFuncArguments.push( - t.objectExpression( - param.properties.filter( - (p) => p.type === 'ObjectProperty' - ) as types.ObjectProperty[] - ) - ) - break - case 'RestElement': - // TODO: Implement me - break - } - } + if (param.type === 'ArrayPattern' || param.type === 'RestElement') { + // TODO: Implement, bail out for now + return + } + } - const filename = state.file.opts.filename - const filenameOffset = filename - ? getBaseDirFromFile(filename).length + 9 // 9 is the length of '/api/src/' - : 0 - const apiFolder = filename - ? filename.substring( - filenameOffset, - filename.substring(filenameOffset).indexOf(nodejsPath.sep) + - filenameOffset - ) - : '?' + const { filename, apiFolder } = getRedwoodPaths(state) - const activeSpanBlock = t.callExpression( - t.memberExpression( - t.identifier('tracer'), - t.identifier('startActiveSpan') - ), - [ - t.stringLiteral(`redwoodjs:api:${apiFolder}:${originalFuncId}`), - t.arrowFunctionExpression( - [t.identifier('span')], - t.blockStatement([ - t.expressionStatement( - t.callExpression( - t.memberExpression( - t.identifier('span'), - t.identifier('setAttribute') - ), - [ - t.stringLiteral('code.function'), - t.stringLiteral(originalFuncId), - ] - ) - ), - t.expressionStatement( - t.callExpression( - t.memberExpression( - t.identifier('span'), - t.identifier('setAttribute') - ), - [ - t.stringLiteral('code.filepath'), - t.stringLiteral(state.file.opts.filename || '?'), - ] - ) - ), - t.tryStatement( - t.blockStatement([ - t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier('innerResult'), - originalFunc.async - ? t.awaitExpression( - t.callExpression( - t.identifier(`_${originalFuncId}`), - originalFuncArguments - ) - ) - : t.callExpression( - t.identifier(`_${originalFuncId}`), - originalFuncArguments - ) - ), - ]), - t.expressionStatement( + const activeSpanBlock = t.callExpression( + t.memberExpression( + t.identifier('RW_OTEL_WRAPPER_TRACER'), + t.identifier('startActiveSpan') + ), + [ + t.stringLiteral(`redwoodjs:api:${apiFolder}:${originalFunctionName}`), + t.arrowFunctionExpression( + [t.identifier('span')], + t.blockStatement([ + t.expressionStatement( + t.callExpression( + t.memberExpression( + t.identifier('span'), + t.identifier('setAttribute') + ), + [ + t.stringLiteral('code.function'), + t.stringLiteral(originalFunctionName), + ] + ) + ), + t.expressionStatement( + t.callExpression( + t.memberExpression( + t.identifier('span'), + t.identifier('setAttribute') + ), + [ + t.stringLiteral('code.filepath'), + t.stringLiteral(filename || '?'), + ] + ) + ), + t.tryStatement( + t.blockStatement([ + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier('RW_OTEL_WRAPPER_INNER_RESULT'), + originalFunction.async + ? t.awaitExpression( t.callExpression( - t.memberExpression( - t.identifier('span'), - t.identifier('end') - ), - [] + t.identifier(wrappedFunctionName), + originalFunctionArgumentsWithoutDefaults ) - ), - t.returnStatement(t.identifier('innerResult')), - ]), - t.catchClause( - t.identifier('error'), - t.blockStatement([ - t.expressionStatement( - t.callExpression( - t.memberExpression( - t.identifier('span'), - t.identifier('recordException') - ), - [t.identifier('error')] - ) + ) + : t.callExpression( + t.identifier(wrappedFunctionName), + originalFunctionArgumentsWithoutDefaults + ) + ), + ]), + t.expressionStatement( + t.callExpression( + t.memberExpression(t.identifier('span'), t.identifier('end')), + [] + ) + ), + t.returnStatement(t.identifier('RW_OTEL_WRAPPER_INNER_RESULT')), + ]), + t.catchClause( + t.identifier('error'), + t.blockStatement([ + t.expressionStatement( + t.callExpression( + t.memberExpression( + t.identifier('span'), + t.identifier('recordException') + ), + [t.identifier('error')] + ) + ), + t.expressionStatement( + t.callExpression( + t.memberExpression( + t.identifier('span'), + t.identifier('setStatus') + ), + [ + t.objectExpression([ + t.objectProperty( + t.identifier('code'), + t.numericLiteral(2) ), - t.expressionStatement( - t.callExpression( - t.memberExpression( - t.identifier('span'), - t.identifier('setStatus') - ), - [ - t.objectExpression([ - t.objectProperty( - t.identifier('code'), - t.numericLiteral(2) + t.objectProperty( + t.identifier('message'), + t.logicalExpression( + '??', + t.optionalMemberExpression( + t.optionalCallExpression( + t.optionalMemberExpression( + t.optionalMemberExpression( + t.identifier('error'), + t.identifier('message'), + false, + true + ), + t.identifier('split'), + false, + true ), - t.objectProperty( - t.identifier('message'), - t.logicalExpression( - '??', + [t.stringLiteral('\n')], + false + ), + t.numericLiteral(0), + true, + false + ), + t.optionalMemberExpression( + t.optionalCallExpression( + t.optionalMemberExpression( + t.optionalCallExpression( t.optionalMemberExpression( - t.optionalCallExpression( - t.optionalMemberExpression( - t.optionalMemberExpression( - t.identifier('error'), - t.identifier('message'), - false, - true - ), - t.identifier('split'), - false, - true - ), - [t.stringLiteral('\n')], - false - ), - t.numericLiteral(0), - true, - false + t.identifier('error'), + t.identifier('toString'), + false, + true ), - t.optionalMemberExpression( - t.optionalCallExpression( - t.optionalMemberExpression( - t.optionalCallExpression( - t.optionalMemberExpression( - t.identifier('error'), - t.identifier('toString'), - false, - true - ), - [], - false - ), - t.identifier('split'), - false, - true - ), - [t.stringLiteral('\n')], - false - ), - t.numericLiteral(0), - true, - false - ) - ) + [], + false + ), + t.identifier('split'), + false, + true ), - ]), - ] - ) - ), - t.expressionStatement( - t.callExpression( - t.memberExpression( - t.identifier('span'), - t.identifier('end') - ), - [] + [t.stringLiteral('\n')], + false + ), + t.numericLiteral(0), + true, + false + ) ) ), - t.throwStatement(t.identifier('error')), - ]) - ) - ), - ]), - originalFunc.async + ]), + ] + ) + ), + t.expressionStatement( + t.callExpression( + t.memberExpression( + t.identifier('span'), + t.identifier('end') + ), + [] + ) + ), + t.throwStatement(t.identifier('error')), + ]) + ) + ), + ]), + originalFunction.async + ), + ] + ) + + const wrapper = t.arrowFunctionExpression( + originalFunction.params, + t.blockStatement( + [ + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier(wrappedFunctionName), + originalFunction + ), + ]), + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier('RW_OTEL_WRAPPER_TRACER'), + t.callExpression( + t.memberExpression( + t.identifier('RW_OTEL_WRAPPER_TRACE'), + t.identifier('getTracer') ), - ] - ) + [t.stringLiteral('redwoodjs')] + ) + ), + ]), + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier('RW_OTEL_WRAPPER_RESULT'), + originalFunction.async + ? t.awaitExpression(activeSpanBlock) + : activeSpanBlock + ), + ]), + t.returnStatement(t.identifier('RW_OTEL_WRAPPER_RESULT')), + ], + originalFunction.body.type === 'BlockStatement' + ? originalFunction.body.directives + : undefined + ), + originalFunction.async + ) - const wrapper = t.arrowFunctionExpression( - originalFunc.params, - t.blockStatement( - [ - t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(`_${originalFuncId}`), - originalFunc - ), - ]), - t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier('tracer'), - t.callExpression( - t.memberExpression( - t.memberExpression( - t.identifier('opentelemetry'), - t.identifier('trace') - ), - t.identifier('getTracer') - ), - [t.stringLiteral('redwoodjs')] - ) - ), - ]), - t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier('result'), - originalFunc.async - ? t.awaitExpression(activeSpanBlock) - : activeSpanBlock - ), - ]), - t.returnStatement(t.identifier('result')), - ], - originalFunc.body.type === 'BlockStatement' - ? originalFunc.body.directives - : undefined - ), - originalFunc.async - ) + // Replace the original function with the wrapped version + declaration.declarations[0].init = wrapper +} - path.node.declaration.declarations[0].init = wrapper - } +export default function ({ types: t }: { types: typeof types }): PluginObj { + return { + name: 'babel-plugin-redwood-otel-wrapping', + visitor: { + Program(path) { + addOpenTelemetryImport(path, t) + }, + ExportNamedDeclaration(path, state) { + wrapExportNamedDeclaration(path, state, t) }, }, } diff --git a/packages/cli/src/commands/experimental/setupOpentelemetryHandler.js b/packages/cli/src/commands/experimental/setupOpentelemetryHandler.js index 7371888f4fbd..eef9373fd67d 100644 --- a/packages/cli/src/commands/experimental/setupOpentelemetryHandler.js +++ b/packages/cli/src/commands/experimental/setupOpentelemetryHandler.js @@ -69,7 +69,7 @@ export const handler = async ({ force, verbose }) => { writeFile( redwoodTomlPath, configContent.concat( - `\n[experimental.opentelemetry]\n\tenabled = true\n\tapiSdk = "${opentelemetryScriptPath}"` + `\n[experimental.opentelemetry]\n\tenabled = true\n\twrapApi = true\n\tapiSdk = "${opentelemetryScriptPath}"` ), { overwriteExisting: true, // redwood.toml always exists diff --git a/packages/internal/src/build/api.ts b/packages/internal/src/build/api.ts index e9f49c977838..bf015bacfa50 100644 --- a/packages/internal/src/build/api.ts +++ b/packages/internal/src/build/api.ts @@ -37,8 +37,11 @@ export const cleanApiBuild = () => { */ export const prebuildApiFiles = (srcFiles: string[]) => { const rwjsPaths = getPaths() + const rwjsConfig = getConfig() const plugins = getApiSideBabelPlugins({ - openTelemetry: getConfig().experimental.opentelemetry.enabled, + openTelemetry: + rwjsConfig.experimental.opentelemetry.enabled && + rwjsConfig.experimental.opentelemetry.wrapApi, }) return srcFiles.map((srcPath) => { diff --git a/packages/project-config/src/__tests__/config.test.ts b/packages/project-config/src/__tests__/config.test.ts index 05728ce72cc0..5e0b777bf9b6 100644 --- a/packages/project-config/src/__tests__/config.test.ts +++ b/packages/project-config/src/__tests__/config.test.ts @@ -57,6 +57,7 @@ describe('getConfig', () => { "opentelemetry": { "apiSdk": undefined, "enabled": false, + "wrapApi": true, }, "rsc": { "enabled": false, diff --git a/packages/project-config/src/config.ts b/packages/project-config/src/config.ts index d975215ff972..17f9ff66b7a5 100644 --- a/packages/project-config/src/config.ts +++ b/packages/project-config/src/config.ts @@ -96,6 +96,7 @@ export interface Config { experimental: { opentelemetry: { enabled: boolean + wrapApi: boolean apiSdk?: string } studio: StudioConfig @@ -158,6 +159,7 @@ const DEFAULT_CONFIG: Config = { experimental: { opentelemetry: { enabled: false, + wrapApi: true, apiSdk: undefined, }, studio: {