Skip to content

Commit 29f8f0f

Browse files
na-trium-144CopilotCopilot
authored
sentryをセットアップ (#214)
* sentryをセットアップ * bundleSizeOptimizationを指定 * sentryとopentelemetryを別ファイルにしてみる * instrumentation.tsでsentryをトップレベルimportしない * 名前を消してみる * sentry/nextjsをexternalPackagesにしてみる * Revert "instrumentation.tsでsentryをトップレベルimportしない" This reverts commit ae747fa. * catchしたエラーとチャットエラーをsentryに送信 * 動作確認用に新しいコミットをpush * eventIDの表示を追加、エラーページのレイアウトを修正 * dev環境でエラーを記録しない * エラーページにEventIDつきフォームリンクを追加、全エラーページをコンポーネントにまとめる * runtime初期化/実行の異常系を `onError` + `fatalError` で分離伝播 (#218) * chore: plan runtime fatal error handling changes Agent-Logs-Url: https://github.com/ut-code/my-code/sessions/3499a4d7-2b75-4046-b6d5-24912d4eaf0a Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> * feat(runtime): add fatalError and onError callback propagation Agent-Logs-Url: https://github.com/ut-code/my-code/sessions/3499a4d7-2b75-4046-b6d5-24912d4eaf0a Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> * revert package-lock.json * fix: restore package-lock from sentry to prevent dependency drift Agent-Logs-Url: https://github.com/ut-code/my-code/sessions/f1dea2f9-037f-4cd9-a9e0-cefe2e569bfa Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> * fix(runtime): avoid bundling typescript.js in server handler Agent-Logs-Url: https://github.com/ut-code/my-code/sessions/1f1e6587-2ca0-4873-86cf-5cc4025a539b Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> * fatalErrorをそんなに目立たせる必要ない * handleRuntimeErrorをplainの関数にし、alertを追加 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> * テストのエラーもsentryに送る * Update app/errorMessage.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update app/errorMessage.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * sentry関連の設定をすべて環境変数に & debugオフ * global-errorページのスタイル修正 & フォームパラメータ修正 * sentryの環境変数をREADMEに書いておく --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2 parents cd80c83 + c95258b commit 29f8f0f

33 files changed

Lines changed: 2376 additions & 838 deletions

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ GOOGLE_CLIENT_ID=
2626
GOOGLE_CLIENT_SECRET=
2727
GITHUB_CLIENT_ID=
2828
GITHUB_CLIENT_SECRET=
29+
SENTRY_ORG=
30+
SENTRY_PROJECT=
31+
SENTRY_URL=
32+
SENTRY_AUTH_TOKEN=
33+
SENTRY_DSN=
2934
```
3035

3136
* チャット用にGeminiのAPIキーまたはOpenRouterのAPIキーのいずれかが必要です。未設定の場合チャットが使えません

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

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

3-
import clsx from "clsx";
43
import { ChatAreaContainer } from "./chat/[chatId]/chatArea";
4+
import { ErrorMessage } from "@/errorMessage";
55

66
export default function Error({
77
error,
@@ -12,20 +12,7 @@ export default function Error({
1212
}) {
1313
return (
1414
<ChatAreaContainer chatId={"error"}>
15-
<p>ページの読み込み中にエラーが発生しました。</p>
16-
<pre
17-
className={clsx(
18-
"border-2 border-current/20 mt-4 rounded-box p-4! bg-base-300! text-base-content!",
19-
"max-w-full whitespace-pre-wrap"
20-
)}
21-
>
22-
{error.message}
23-
</pre>
24-
{error.digest && (
25-
<p className="mt-2 text-sm text-base-content/50">
26-
Digest: {error.digest}
27-
</p>
28-
)}
15+
<ErrorMessage error={error} />
2916
</ChatAreaContainer>
3017
);
3118
}

app/(docs)/@docs/[lang]/[pageId]/chatForm.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
"use client";
22

3-
import { useState, FormEvent, useEffect, useRef, useCallback, useMemo } from "react";
3+
import {
4+
useState,
5+
FormEvent,
6+
useEffect,
7+
useRef,
8+
useCallback,
9+
useMemo,
10+
} from "react";
411
// import useSWR from "swr";
512
// import {
613
// getQuestionExample,
@@ -13,6 +20,7 @@ import { usePathname, useRouter } from "next/navigation";
1320
import { ChatStreamEvent } from "@/api/chat/route";
1421
import { useStreamingChatContext } from "@/(docs)/streamingChatContext";
1522
import { revalidateChatAction } from "@/actions/revalidateChat";
23+
import { captureException } from "@sentry/nextjs";
1624

1725
interface ChatFormProps {
1826
path: PagePath;
@@ -84,7 +92,6 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) {
8492
}, [exampleChoice]);
8593

8694
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
87-
8895
let userQuestion = inputValue;
8996
if (!userQuestion && exampleData.length > 0 && exampleChoice) {
9097
// 質問が空欄なら、質問例を使用
@@ -114,7 +121,8 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) {
114121
execResults,
115122
}),
116123
});
117-
} catch {
124+
} catch (e) {
125+
captureException(e);
118126
setErrorMessage("AIへの接続に失敗しました");
119127
setIsLoading(false);
120128
return;
@@ -184,12 +192,14 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) {
184192
streamingChatContext.finishStreaming();
185193
router.refresh();
186194
}
187-
} catch {
195+
} catch (e) {
196+
captureException(e);
188197
// ignore JSON parse errors
189198
}
190199
}
191200
}
192201
} catch (err) {
202+
captureException(err);
193203
console.error("Stream reading failed:", err);
194204
// ナビゲーション後のエラーはストリーミングを終了してローディングを止める
195205
if (!navigated) {

app/(docs)/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default function WorkspaceLayout({
1515
return (
1616
<StreamingChatProvider>
1717
<ChatAreaStateProvider>
18-
<div className="w-full flex flex-row">
18+
<div className="flex-1 w-full flex flex-row">
1919
{docs}
2020

2121
{chat}

app/about/license/ThirdPartyLicenses.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"use client";
22

3-
import { StyledSyntaxHighlighter } from "@/markdown/styledSyntaxHighlighter";
4-
import { langConstants } from "@my-code/runtime/languages";
3+
import { FallbackPre } from "@/markdown/styledSyntaxHighlighter";
54
import { useState } from "react";
65

76
export interface LicenseEntry {
@@ -80,12 +79,7 @@ export function ThirdPartyLicenses({ licenses }: { licenses: LicenseEntry[] }) {
8079
</p>
8180
)}
8281
{pkg.licenseText && (
83-
<StyledSyntaxHighlighter
84-
className="text-sm"
85-
language={langConstants(undefined)}
86-
>
87-
{pkg.licenseText}
88-
</StyledSyntaxHighlighter>
82+
<FallbackPre className="text-sm">{pkg.licenseText}</FallbackPre>
8983
)}
9084
</div>
9185
</div>

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
}

0 commit comments

Comments
 (0)