Skip to content

Commit 725e96e

Browse files
committed
sentryをセットアップ
1 parent 51196ce commit 725e96e

15 files changed

+2017
-740
lines changed

app/(docs)/@chat/error.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import clsx from "clsx";
44
import { ChatAreaContainer } from "./chat/[chatId]/chatArea";
5+
import { useEffect } from "react";
6+
import { captureException } from "@sentry/nextjs";
57

68
export default function Error({
79
error,
@@ -10,6 +12,10 @@ export default function Error({
1012
error: Error & { digest?: string };
1113
reset: () => void;
1214
}) {
15+
useEffect(() => {
16+
captureException(error);
17+
}, [error]);
18+
1319
return (
1420
<ChatAreaContainer chatId={"error"}>
1521
<p>ページの読み込み中にエラーが発生しました。</p>

app/actions/clearUserCache.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,39 @@ import { initContext, cacheKeyForPage } from "@/lib/chatHistory";
44
import { updateTag } from "next/cache";
55
import { getPagesList } from "@/lib/docs";
66
import { isCloudflare } from "@/lib/detectCloudflare";
7+
import { headers } from "next/headers";
8+
import { withServerActionInstrumentation } from "@sentry/nextjs";
79

810
export async function clearUserCacheAction() {
9-
const ctx = await initContext();
10-
if (!ctx.userId) return;
11+
return withServerActionInstrumentation(
12+
"clearUserCacheAction", // Action name for Sentry
13+
{
14+
headers: await headers(), // Connect client and server traces
15+
recordResponse: true, // Include response data
16+
},
17+
async () => {
18+
const ctx = await initContext();
19+
if (!ctx.userId) return;
1120

12-
const pagesList = await getPagesList();
13-
for (const lang of pagesList) {
14-
for (const page of lang.pages) {
15-
updateTag(cacheKeyForPage({ lang: lang.id, page: page.slug }, ctx.userId));
16-
}
17-
}
21+
const pagesList = await getPagesList();
22+
for (const lang of pagesList) {
23+
for (const page of lang.pages) {
24+
updateTag(
25+
cacheKeyForPage({ lang: lang.id, page: page.slug }, ctx.userId)
26+
);
27+
}
28+
}
1829

19-
if (isCloudflare()) {
20-
const cache = await caches.open("chatHistory");
21-
for (const lang of pagesList) {
22-
for (const page of lang.pages) {
23-
await cache.delete(
24-
cacheKeyForPage({ lang: lang.id, page: page.slug }, ctx.userId)
25-
);
30+
if (isCloudflare()) {
31+
const cache = await caches.open("chatHistory");
32+
for (const lang of pagesList) {
33+
for (const page of lang.pages) {
34+
await cache.delete(
35+
cacheKeyForPage({ lang: lang.id, page: page.slug }, ctx.userId)
36+
);
37+
}
38+
}
2639
}
2740
}
28-
}
41+
);
2942
}

app/actions/deleteChat.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,30 @@ import { z } from "zod";
44
import { deleteChat, initContext, revalidateChat } from "@/lib/chatHistory";
55
import { section } from "@/schema/chat";
66
import { eq } from "drizzle-orm";
7+
import { withServerActionInstrumentation } from "@sentry/nextjs";
8+
import { headers } from "next/headers";
79

810
export async function deleteChatAction(chatId: string) {
9-
chatId = z.uuid().parse(chatId);
10-
const ctx = await initContext();
11-
if (!ctx.userId) {
12-
throw new Error("Not authenticated");
13-
}
14-
const deletedChat = await deleteChat(chatId, ctx);
11+
return withServerActionInstrumentation(
12+
"deleteChatAction", // Action name for Sentry
13+
{
14+
headers: await headers(), // Connect client and server traces
15+
recordResponse: true, // Include response data
16+
},
17+
async () => {
18+
chatId = z.uuid().parse(chatId);
19+
const ctx = await initContext();
20+
if (!ctx.userId) {
21+
throw new Error("Not authenticated");
22+
}
23+
const deletedChat = await deleteChat(chatId, ctx);
1524

16-
const targetSection = await ctx.drizzle.query.section.findFirst({
17-
where: eq(section.sectionId, deletedChat[0].sectionId),
18-
});
19-
if (targetSection) {
20-
await revalidateChat(chatId, ctx.userId, targetSection.pagePath);
21-
}
25+
const targetSection = await ctx.drizzle.query.section.findFirst({
26+
where: eq(section.sectionId, deletedChat[0].sectionId),
27+
});
28+
if (targetSection) {
29+
await revalidateChat(chatId, ctx.userId, targetSection.pagePath);
30+
}
31+
}
32+
);
2233
}

app/actions/getRedirectFromChat.ts

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,44 @@ import { initContext } from "@/lib/chatHistory";
55
import { LangId, PageSlug } from "@/lib/docs";
66
import { chat, section } from "@/schema/chat";
77
import { and, eq } from "drizzle-orm";
8+
import { setExtra, withServerActionInstrumentation } from "@sentry/nextjs";
9+
import { headers } from "next/headers";
810

911
export async function getRedirectFromChat(chatId: string): Promise<string> {
10-
chatId = z.uuid().parse(chatId);
11-
12-
const { drizzle, userId } = await initContext();
13-
if (!userId) {
14-
throw new Error("Not authenticated");
15-
}
16-
17-
const chatData = (await drizzle.query.chat.findFirst({
18-
where: and(eq(chat.chatId, chatId), eq(chat.userId, userId)),
19-
with: {
20-
section: true,
12+
return withServerActionInstrumentation(
13+
"getRedirectFromChat", // Action name for Sentry
14+
{
15+
headers: await headers(), // Connect client and server traces
16+
recordResponse: true, // Include response data
2117
},
22-
})) as
23-
| (typeof chat.$inferSelect & {
24-
section: typeof section.$inferSelect;
25-
})
26-
| undefined;
27-
if (!chatData?.section) {
28-
throw new Error("Chat or section not found");
29-
}
30-
const [lang, page] = (chatData.section.pagePath.split("/") ?? []) as [
31-
LangId,
32-
PageSlug,
33-
];
34-
return `/${lang}/${page}#${chatData.sectionId}`;
18+
async () => {
19+
setExtra("args", { chatId });
20+
21+
chatId = z.uuid().parse(chatId);
22+
23+
const { drizzle, userId } = await initContext();
24+
if (!userId) {
25+
throw new Error("Not authenticated");
26+
}
27+
28+
const chatData = (await drizzle.query.chat.findFirst({
29+
where: and(eq(chat.chatId, chatId), eq(chat.userId, userId)),
30+
with: {
31+
section: true,
32+
},
33+
})) as
34+
| (typeof chat.$inferSelect & {
35+
section: typeof section.$inferSelect;
36+
})
37+
| undefined;
38+
if (!chatData?.section) {
39+
throw new Error("Chat or section not found");
40+
}
41+
const [lang, page] = (chatData.section.pagePath.split("/") ?? []) as [
42+
LangId,
43+
PageSlug,
44+
];
45+
return `/${lang}/${page}#${chatData.sectionId}`;
46+
}
47+
);
3548
}

app/actions/revalidateChat.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,38 @@
22

33
import { initContext, revalidateChat } from "@/lib/chatHistory";
44
import { PagePath, PagePathSchema } from "@/lib/docs";
5+
import { setExtra, withServerActionInstrumentation } from "@sentry/nextjs";
6+
import { headers } from "next/headers";
57
import { z } from "zod";
68

79
export async function revalidateChatAction(
810
chatId: string,
911
pagePath: string | PagePath
1012
) {
11-
chatId = z.uuid().parse(chatId);
12-
if (typeof pagePath === "string") {
13-
if (!/^[a-z0-9_-]+\/[a-z0-9_-]+$/.test(pagePath)) {
14-
throw new Error("Invalid pagePath format");
13+
return withServerActionInstrumentation(
14+
"revalidateChatAction", // Action name for Sentry
15+
{
16+
headers: await headers(), // Connect client and server traces
17+
recordResponse: true, // Include response data
18+
},
19+
async () => {
20+
setExtra("args", { chatId, pagePath });
21+
22+
chatId = z.uuid().parse(chatId);
23+
if (typeof pagePath === "string") {
24+
if (!/^[a-z0-9_-]+\/[a-z0-9_-]+$/.test(pagePath)) {
25+
throw new Error("Invalid pagePath format");
26+
}
27+
const [lang, page] = pagePath.split("/");
28+
pagePath = PagePathSchema.parse({ lang, page });
29+
} else {
30+
pagePath = PagePathSchema.parse(pagePath);
31+
}
32+
const ctx = await initContext();
33+
if (!ctx.userId) {
34+
throw new Error("Not authenticated");
35+
}
36+
await revalidateChat(chatId, ctx.userId, pagePath);
1537
}
16-
const [lang, page] = pagePath.split("/");
17-
pagePath = PagePathSchema.parse({ lang, page });
18-
} else {
19-
pagePath = PagePathSchema.parse(pagePath);
20-
}
21-
const ctx = await initContext();
22-
if (!ctx.userId) {
23-
throw new Error("Not authenticated");
24-
}
25-
await revalidateChat(chatId, ctx.userId, pagePath);
38+
);
2639
}

app/error.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"use client"; // Error boundaries must be Client Components
22

3+
import { captureException } from "@sentry/nextjs";
34
import clsx from "clsx";
45
import Link from "next/link";
6+
import { useEffect } from "react";
57

68
export default function ErrorPage({
79
error,
@@ -10,6 +12,10 @@ export default function ErrorPage({
1012
error: unknown;
1113
reset: () => void;
1214
}) {
15+
useEffect(() => {
16+
captureException(error);
17+
}, [error]);
18+
1319
return (
1420
<div className="p-4 flex-1 w-max max-w-full mx-auto flex flex-col items-center justify-center">
1521
<h1 className="text-2xl font-bold mb-4">エラー</h1>

app/global-error.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"use client"; // Error boundaries must be Client Components
2+
3+
import { captureException } from "@sentry/nextjs";
4+
import clsx from "clsx";
5+
import Link from "next/link";
6+
import { useEffect } from "react";
7+
8+
export default function ErrorPage({
9+
error,
10+
reset,
11+
}: {
12+
error: unknown;
13+
reset: () => void;
14+
}) {
15+
useEffect(() => {
16+
captureException(error);
17+
}, [error]);
18+
19+
return (
20+
<div className="p-4 flex-1 w-max max-w-full mx-auto flex flex-col items-center justify-center">
21+
<h1 className="text-2xl font-bold mb-4">エラー</h1>
22+
<p>ページの読み込み中にエラーが発生しました。</p>
23+
<pre
24+
className={clsx(
25+
"border-2 border-current/20 mt-4 rounded-box p-4! bg-base-300! text-base-content!",
26+
"max-w-full whitespace-pre-wrap"
27+
)}
28+
>
29+
{error instanceof Error ? error.message : String(error)}
30+
</pre>
31+
{"digest" in (error as { digest: string }) && (
32+
<p className="mt-2 text-sm text-base-content/50">
33+
Digest: {(error as { digest: string }).digest}
34+
</p>
35+
)}
36+
<div className="divider w-full self-auto!" />
37+
<div className="flex flex-row gap-4">
38+
<button
39+
className="btn btn-warning"
40+
onClick={
41+
// Attempt to recover by trying to re-render the segment
42+
() => reset()
43+
}
44+
>
45+
やりなおす
46+
</button>
47+
<Link href="/" className="btn btn-primary">
48+
トップに戻る
49+
</Link>
50+
</div>
51+
</div>
52+
);
53+
}

app/terminal/page.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ export default function RuntimeTestPage() {
5353
<Heading level={2}>Xterm.js Colors</Heading>
5454
<AnsiColorSample />
5555

56+
<button
57+
className="btn mt-4"
58+
onClick={() => {
59+
throw new Error("Sentry Test Error");
60+
}}
61+
>
62+
Sentry Test Error
63+
</button>
64+
5665
<Heading level={2}>自動テスト</Heading>
5766
<MochaTest />
5867
</div>

instrumentation-client.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as Sentry from "@sentry/nextjs";
2+
Sentry.init({
3+
dsn: "https://db719a3358e14ebc86cc975ea04b0dac@bugsink.utcode.net/1",
4+
// Adds request headers and IP for users
5+
sendDefaultPii: true,
6+
});
7+
8+
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;

instrumentation.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as Sentry from "@sentry/nextjs";
2+
export async function register() {
3+
if (process.env.NEXT_RUNTIME === "nodejs") {
4+
await import("./sentry.server.config");
5+
}
6+
if (process.env.NEXT_RUNTIME === "edge") {
7+
await import("./sentry.edge.config");
8+
}
9+
}
10+
// Capture errors from Server Components, middleware, and proxies
11+
export const onRequestError = Sentry.captureRequestError;

0 commit comments

Comments
 (0)