Merged in feat/sentry (pull request #1089)

This commit is contained in:
Linus Flood
2024-12-19 10:35:20 +00:00
committed by Joakim Jäderberg
parent e0c5b59860
commit 3982b1ba56
32 changed files with 2715 additions and 429 deletions

View File

@@ -52,3 +52,6 @@ ENABLE_BOOKING_FLOW="false"
ENABLE_BOOKING_WIDGET="false"
ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH="false"
SHOW_SITE_WIDE_ALERT="false"
NEXT_PUBLIC_SENTRY_ENVIRONMENT="test"
NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE="0"

2
.gitignore vendored
View File

@@ -47,3 +47,5 @@ certificates
# localfile with all the CSS variables exported from design system
variables.css
# Sentry Config File
.env.sentry-build-plugin

View File

@@ -1,5 +1,5 @@
"use client"
import * as Sentry from "@sentry/nextjs"
import { useEffect } from "react"
export default function Error({
@@ -8,7 +8,10 @@ export default function Error({
error: Error & { digest?: string }
}) {
useEffect(() => {
if (!error) return
console.error({ breadcrumbsError: error })
Sentry.captureException(error)
}, [error])
return (

View File

@@ -1,5 +1,6 @@
"use client"
import * as Sentry from "@sentry/nextjs"
import { useEffect } from "react"
export default function Error({
@@ -8,7 +9,10 @@ export default function Error({
error: Error & { digest?: string }
}) {
useEffect(() => {
console.error({ breadcrumbsError: error })
if (!error) return
console.error(error)
Sentry.captureException(error)
}, [error])
return (

View File

@@ -1,8 +1,17 @@
"use client"
import * as Sentry from "@sentry/nextjs"
import { useEffect } from "react"
import type { ErrorPage } from "@/types/next/error"
export default function ProfileError({ error }: ErrorPage) {
console.error(error)
useEffect(() => {
if (!error) return
console.error(error)
Sentry.captureException(error)
}, [error])
return <h1>Error happened, Profile</h1>
}

View File

@@ -1,5 +1,5 @@
"use client" // Error components must be Client Components
import * as Sentry from "@sentry/nextjs"
import {
useParams,
usePathname,
@@ -33,13 +33,17 @@ export default function Error({
const isFirstLoadRef = useRef<boolean>(true)
useEffect(() => {
// Log the error to an error reporting service
if (!error) return
console.error(error)
if (error.message === SESSION_EXPIRED) {
const loginUrl = login[params.lang]
window.location.assign(loginUrl)
return
}
Sentry.captureException(error)
}, [error, params.lang])
useEffect(() => {

View File

@@ -1,5 +1,19 @@
"use client"
export default function Error() {
import * as Sentry from "@sentry/nextjs"
import { useEffect } from "react"
export default function Error({
error,
}: {
error: Error & { digest?: string }
}) {
useEffect(() => {
if (!error) return
console.error(error)
Sentry.captureException(error)
}, [error])
return null
}

View File

@@ -1,5 +1,8 @@
"use client"
import * as Sentry from "@sentry/nextjs"
import { useEffect } from "react"
import styles from "./global-error.module.css"
export default function GlobalError({
@@ -8,6 +11,11 @@ export default function GlobalError({
error: Error & { digest?: string }
}) {
console.log({ global_error: error })
useEffect(() => {
Sentry.captureException(error)
}, [error])
return (
<html>
<body>

View File

@@ -1,4 +1,4 @@
import { LangRoute } from "@/types/routes"
import type { LangRoute } from "@/types/routes"
export const signup: LangRoute = {
en: "/en/scandic-friends/join",

5
env/client.ts vendored
View File

@@ -5,6 +5,8 @@ export const env = createEnv({
client: {
NEXT_PUBLIC_NODE_ENV: z.enum(["development", "test", "production"]),
NEXT_PUBLIC_PORT: z.string().default("3000"),
NEXT_PUBLIC_SENTRY_ENVIRONMENT: z.string().default("development"),
NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE: z.coerce.number().default(0.01),
NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE: z
.string()
// only allow "true" or "false"
@@ -16,6 +18,9 @@ export const env = createEnv({
runtimeEnv: {
NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_PORT: process.env.NEXT_PUBLIC_PORT,
NEXT_PUBLIC_SENTRY_ENVIRONMENT: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT,
NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE:
process.env.NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE,
NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE:
process.env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE,
},

4
env/server.ts vendored
View File

@@ -129,6 +129,8 @@ export const env = createEnv({
// transform to boolean
.transform((s) => s === "true")
.default("false"),
SENTRY_ENVIRONMENT: z.string().default("development"),
SENTRY_SERVER_SAMPLERATE: z.coerce.number().default(0.01),
},
emptyStringAsUndefined: true,
runtimeEnv: {
@@ -193,5 +195,7 @@ export const env = createEnv({
ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH:
process.env.ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH,
SHOW_SITE_WIDE_ALERT: process.env.SHOW_SITE_WIDE_ALERT,
SENTRY_ENVIRONMENT: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT,
SENTRY_SERVER_SAMPLERATE: process.env.SENTRY_SERVER_SAMPLERATE,
},
})

View File

@@ -1,6 +1,15 @@
import * as Sentry from "@sentry/nextjs"
import { env } from "./env/server"
export async function register() {
await configureSentry()
await configureApplicationInsights()
}
export const onRequestError = Sentry.captureRequestError
async function configureApplicationInsights() {
if (
process.env.NEXT_RUNTIME === "nodejs" &&
env.APPLICATION_INSIGHTS_CONNECTION_STRING
@@ -11,11 +20,8 @@ export async function register() {
const { PeriodicExportingMetricReader } = await import(
"@opentelemetry/sdk-metrics"
)
const connectionString: string = env.APPLICATION_INSIGHTS_CONNECTION_STRING
const traceExporter = new AzureMonitorTraceExporter({ connectionString })
const azureMetricExporter = new AzureMonitorMetricExporter({
connectionString,
})
@@ -23,7 +29,6 @@ export async function register() {
exporter: azureMetricExporter,
exportIntervalMillis: 10000,
})
registerOTel({
serviceName: "scandic-web",
traceExporter,
@@ -31,3 +36,14 @@ export async function register() {
})
}
}
async function configureSentry() {
switch (process.env.NEXT_RUNTIME) {
case "edge": {
await import("./sentry.edge.config")
}
case "nodejs": {
await import("./sentry.server.config")
}
}
}

View File

@@ -1,4 +1,5 @@
import { NextMiddleware, NextResponse } from "next/server"
import * as Sentry from "@sentry/nextjs"
import { type NextMiddleware, NextResponse } from "next/server"
import { Lang } from "@/constants/languages"
@@ -63,20 +64,22 @@ export const middleware: NextMiddleware = async (request, event) => {
if (middleware.matcher(request)) {
const result = await middleware.middleware(request, event)
const _continue = result?.headers.get("x-continue")
if (_continue) {
continue
}
// Clean up internal headers
result?.headers.delete("x-sh-origin")
return result
}
}
} catch (e) {
if (e instanceof NextResponse && e.status) {
const cause = await e.json()
console.error(`NextResponse Error in middleware`)
console.error(cause)
console.error(`NextResponse Error in middleware`, cause)
Sentry.captureException(cause)
return NextResponse.rewrite(
new URL(`/${lang}/middleware-error/${e.status}`, request.nextUrl),
@@ -90,8 +93,9 @@ export const middleware: NextMiddleware = async (request, event) => {
)
}
console.error(`Error in middleware`)
console.error(e)
console.error(`Error in middleware`, e)
Sentry.captureException(e)
return NextResponse.rewrite(
new URL(`/${lang}/middleware-error/500`, request.nextUrl),
{

View File

@@ -1,4 +1,4 @@
import { NextResponse } from "next/server"
import { type NextMiddleware, NextResponse } from "next/server"
import { notFound } from "@/server/errors/next"
@@ -7,9 +7,7 @@ import { removeTrailingSlash } from "@/utils/url"
import { fetchAndCacheEntry, getDefaultRequestHeaders } from "./utils"
import type { NextMiddleware } from "next/server"
import { MiddlewareMatcher } from "@/types/middleware"
import type { MiddlewareMatcher } from "@/types/middleware"
export const middleware: NextMiddleware = async (request) => {
const { nextUrl } = request

View File

@@ -1,6 +1,6 @@
import { NextMiddleware, NextResponse } from "next/server"
import { type NextMiddleware, NextResponse } from "next/server"
import { MiddlewareMatcher } from "@/types/middleware"
import type { MiddlewareMatcher } from "@/types/middleware"
/*
Middleware function to normalize date formats to support

View File

@@ -1,3 +1,4 @@
import * as Sentry from "@sentry/nextjs"
import createJiti from "jiti"
import { fileURLToPath } from "url"
@@ -292,4 +293,23 @@ const nextConfig = {
},
}
export default nextConfig
export default Sentry.withSentryConfig(nextConfig, {
org: "scandic-hotels",
project: "scandic-web",
enabled: process.env.NODE_ENV !== "development",
authToken: process.env.SENTRY_AUTH_TOKEN,
// Only print logs for uploading source maps in CI
silent: !process.env.CI,
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Automatically annotate React components to show their full name in breadcrumbs and session replay
reactComponentAnnotation: {
enabled: true,
},
hideSourceMaps: true,
disableLogger: true,
})

2860
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,11 +24,11 @@
"test:unit:watch": "jest --watch"
},
"dependencies": {
"@azure/monitor-opentelemetry-exporter": "^1.0.0-beta.24",
"@azure/monitor-opentelemetry-exporter": "^1.0.0-beta.27",
"@contentstack/live-preview-utils": "^3.0.0",
"@hookform/error-message": "^2.0.1",
"@hookform/resolvers": "^3.3.4",
"@netlify/plugin-nextjs": "^5.1.1",
"@netlify/plugin-nextjs": "^5.9.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/sdk-metrics": "^1.25.1",
"@radix-ui/react-dialog": "^1.1.1",
@@ -36,6 +36,7 @@
"@radix-ui/react-visually-hidden": "^1.1.0",
"@react-aria/ssr": "^3.9.5",
"@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.9",
"@sentry/nextjs": "^8.41.0",
"@t3-oss/env-nextjs": "^0.9.2",
"@tanstack/react-query": "^5.28.6",
"@tanstack/react-table": "^8.20.5",

17
sentry.client.config.ts Normal file
View File

@@ -0,0 +1,17 @@
import * as Sentry from "@sentry/nextjs"
import { env } from "./env/client"
Sentry.init({
dsn: "https://fe39c070b4154e2f9cc35f0e5de0aedb@o4508102497206272.ingest.de.sentry.io/4508102500286544",
environment: env.NEXT_PUBLIC_SENTRY_ENVIRONMENT,
enabled: env.NEXT_PUBLIC_SENTRY_ENVIRONMENT !== "development",
tracesSampleRate: env.NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE,
// Set profilesSampleRate to 1.0 to profile every transaction.
// Since profilesSampleRate is relative to tracesSampleRate,
// the final profiling rate can be computed as tracesSampleRate * profilesSampleRate
// For example, a tracesSampleRate of 0.5 and profilesSampleRate of 0.5 would
// result in 25% of transactions being profiled (0.5*0.5=0.25)
profilesSampleRate: 0.01,
})

11
sentry.edge.config.ts Normal file
View File

@@ -0,0 +1,11 @@
import * as Sentry from "@sentry/nextjs"
import { env } from "./env/server"
Sentry.init({
dsn: "https://fe39c070b4154e2f9cc35f0e5de0aedb@o4508102497206272.ingest.de.sentry.io/4508102500286544",
environment: env.SENTRY_ENVIRONMENT,
enabled: env.SENTRY_ENVIRONMENT !== "development",
tracesSampleRate: env.SENTRY_SERVER_SAMPLERATE,
})

10
sentry.server.config.ts Normal file
View File

@@ -0,0 +1,10 @@
import * as Sentry from "@sentry/nextjs"
import { env } from "./env/server"
Sentry.init({
dsn: "https://fe39c070b4154e2f9cc35f0e5de0aedb@o4508102497206272.ingest.de.sentry.io/4508102500286544",
environment: env.SENTRY_ENVIRONMENT,
enabled: env.SENTRY_ENVIRONMENT !== "development",
tracesSampleRate: env.SENTRY_SERVER_SAMPLERATE,
})

View File

@@ -1,5 +1,5 @@
import { RoomData } from "@/types/hotel"
import { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { RoomData } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export function getBookedHotelRoom(
rooms: RoomData[] | undefined,

View File

@@ -1,6 +1,5 @@
import { metrics } from "@opentelemetry/api"
import { Lang } from "@/constants/languages"
import {
GetAccountPage,
GetAccountPageRefs,
@@ -26,6 +25,7 @@ import type {
GetAccountPageRefsSchema,
GetAccountPageSchema,
} from "@/types/trpc/routers/contentstack/accountPage"
import type { Lang } from "@/constants/languages"
const meter = metrics.getMeter("trpc.accountPage")

View File

@@ -1,4 +1,3 @@
import { Lang } from "@/constants/languages"
import { batchRequest } from "@/lib/graphql/batchRequest"
import {
GetContentPage,
@@ -19,6 +18,7 @@ import {
type TrackingSDKPageData,
} from "@/types/components/tracking"
import type { GetContentPageSchema } from "@/types/trpc/routers/contentstack/contentPage"
import type { Lang } from "@/constants/languages"
export const contentPageQueryRouter = router({
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {

View File

@@ -1,6 +1,5 @@
import { metrics } from "@opentelemetry/api"
import { Lang } from "@/constants/languages"
import {
GetLoyaltyPage,
GetLoyaltyPageRefs,
@@ -20,12 +19,13 @@ import { getConnections } from "./utils"
import {
TrackingChannelEnum,
TrackingSDKPageData,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import type {
GetLoyaltyPageRefsSchema,
GetLoyaltyPageSchema,
} from "@/types/trpc/routers/contentstack/loyaltyPage"
import type { Lang } from "@/constants/languages"
const meter = metrics.getMeter("trpc.loyaltyPage")
// OpenTelemetry metrics: LoyaltyPage

View File

@@ -1,3 +1,4 @@
import * as Sentry from "@sentry/node"
import { initTRPC } from "@trpc/server"
import { experimental_nextAppDirCaller } from "@trpc/server/adapters/next-app-dir"
import { ZodError } from "zod"
@@ -36,31 +37,42 @@ const t = initTRPC
},
})
const sentryMiddleware = t.middleware(
Sentry.trpcMiddleware({
attachRpcInput: true,
})
)
export const { createCallerFactory, mergeRouters, router } = t
export const publicProcedure = t.procedure
export const contentstackBaseProcedure = t.procedure.use(async function (opts) {
if (!opts.ctx.lang) {
// When fetching data client side with TRPC we don't pass through middlewares and therefore do not get the lang through headers
// We can then pass lang as an input in the request and set it to the context in the procedure
const input = await opts.getRawInput()
const parsedInput = langInput.safeParse(input)
if (!parsedInput.success) {
throw badRequestError("Missing Lang in tRPC context")
const baseProcedure = t.procedure.use(sentryMiddleware)
export const publicProcedure = baseProcedure
export const contentstackBaseProcedure = baseProcedure.use(
async function (opts) {
if (!opts.ctx.lang) {
// When fetching data client side with TRPC we don't pass through middlewares and therefore do not get the lang through headers
// We can then pass lang as an input in the request and set it to the context in the procedure
const input = await opts.getRawInput()
const parsedInput = langInput.safeParse(input)
if (!parsedInput.success) {
throw badRequestError("Missing Lang in tRPC context")
}
return opts.next({
ctx: {
lang: parsedInput.data.lang,
},
})
}
return opts.next({
ctx: {
lang: parsedInput.data.lang,
lang: opts.ctx.lang,
},
})
}
return opts.next({
ctx: {
lang: opts.ctx.lang,
},
})
})
)
export const contentstackExtendedProcedureUID = contentstackBaseProcedure.use(
async function (opts) {
if (!opts.ctx.uid) {
@@ -74,7 +86,7 @@ export const contentstackExtendedProcedureUID = contentstackBaseProcedure.use(
})
}
)
export const protectedProcedure = t.procedure.use(async function (opts) {
export const protectedProcedure = baseProcedure.use(async function (opts) {
const authRequired = opts.meta?.authRequired ?? true
const session = await opts.ctx.auth()
if (!authRequired && env.NODE_ENV === "development") {
@@ -99,7 +111,7 @@ export const protectedProcedure = t.procedure.use(async function (opts) {
})
})
export const safeProtectedProcedure = t.procedure.use(async function (opts) {
export const safeProtectedProcedure = baseProcedure.use(async function (opts) {
const authRequired = opts.meta?.authRequired ?? true
let session: Session | null = await opts.ctx.auth()
@@ -121,7 +133,7 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) {
})
})
export const serviceProcedure = t.procedure.use(async function (opts) {
export const serviceProcedure = baseProcedure.use(async (opts) => {
const { access_token } = await getServiceToken()
if (!access_token) {
throw internalServerError(`[serviceProcedure] No service token`)
@@ -133,7 +145,7 @@ export const serviceProcedure = t.procedure.use(async function (opts) {
})
})
export const serverActionProcedure = t.procedure.experimental_caller(
export const serverActionProcedure = baseProcedure.experimental_caller(
experimental_nextAppDirCaller({
createContext,
normalizeFormData: true,

View File

@@ -1,14 +1,12 @@
import { z } from "zod"
import type { z } from "zod"
import {
import type {
priceSchema,
Product,
productTypePriceSchema,
RoomConfiguration,
} from "@/server/routers/hotels/output"
import { RoomPackage } from "./roomFilter"
import type { RoomPackage } from "./roomFilter"
import type { RateCode } from "./selectRate"
type ProductPrice = z.output<typeof productTypePriceSchema>

View File

@@ -1,6 +1,5 @@
import { Product, RoomConfiguration } from "@/server/routers/hotels/output"
import { ChildBedMapEnum } from "../../bookingWidget/enums"
import type { Product, RoomConfiguration } from "@/server/routers/hotels/output"
import type { ChildBedMapEnum } from "../../bookingWidget/enums"
export interface Child {
bed: ChildBedMapEnum

View File

@@ -1,6 +1,6 @@
import { sidePanelVariants } from "@/components/HotelReservation/SidePanel/variants"
import type { VariantProps } from "class-variance-authority"
import type { sidePanelVariants } from "@/components/HotelReservation/SidePanel/variants"
export interface SidePanelProps
extends VariantProps<typeof sidePanelVariants> {}

View File

@@ -1,6 +1,5 @@
import { MembershipLevel } from "@/constants/membershipLevels"
import type { Lang } from "@/constants/languages"
import type { MembershipLevel } from "@/constants/membershipLevels"
export enum TrackingChannelEnum {
"scandic-friends" = "scandic-friends",

View File

@@ -1,8 +1,7 @@
import { z } from "zod"
import type { z } from "zod"
import { bookingConfirmationSchema } from "@/server/routers/booking/output"
import { Hotel, RoomData } from "@/types/hotel"
import type { Hotel, RoomData } from "@/types/hotel"
import type { bookingConfirmationSchema } from "@/server/routers/booking/output"
export interface BookingConfirmationSchema
extends z.output<typeof bookingConfirmationSchema> {}

View File

@@ -1,4 +1,4 @@
import { TrackingPosition, TrackingSDKData } from "@/types/components/tracking"
import type { TrackingPosition, TrackingSDKData } from "@/types/components/tracking"
export function trackClick(name: string) {
pushToDataLayer({