Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
725e96e
sentryをセットアップ
na-trium-144 Apr 7, 2026
d01e487
bundleSizeOptimizationを指定
na-trium-144 Apr 7, 2026
7546d08
sentryとopentelemetryを別ファイルにしてみる
na-trium-144 Apr 7, 2026
ae747fa
instrumentation.tsでsentryをトップレベルimportしない
na-trium-144 Apr 7, 2026
0267ef6
名前を消してみる
na-trium-144 Apr 7, 2026
b673c21
sentry/nextjsをexternalPackagesにしてみる
na-trium-144 Apr 7, 2026
ac0474c
Revert "instrumentation.tsでsentryをトップレベルimportしない"
na-trium-144 Apr 7, 2026
6715573
catchしたエラーとチャットエラーをsentryに送信
na-trium-144 Apr 7, 2026
9e32c6d
動作確認用に新しいコミットをpush
na-trium-144 Apr 9, 2026
fba2718
eventIDの表示を追加、エラーページのレイアウトを修正
na-trium-144 Apr 9, 2026
f2f9a04
dev環境でエラーを記録しない
na-trium-144 Apr 9, 2026
8d2d357
Merge remote-tracking branch 'origin/main' into sentry
na-trium-144 Apr 16, 2026
f3d76b7
エラーページにEventIDつきフォームリンクを追加、全エラーページをコンポーネントにまとめる
na-trium-144 Apr 16, 2026
e270b49
runtime初期化/実行の異常系を `onError` + `fatalError` で分離伝播 (#218)
Copilot Apr 17, 2026
99c4ca7
テストのエラーもsentryに送る
na-trium-144 Apr 17, 2026
eb2c861
Update app/errorMessage.tsx
na-trium-144 Apr 17, 2026
8e6fc66
Update app/errorMessage.tsx
na-trium-144 Apr 17, 2026
9368760
sentry関連の設定をすべて環境変数に & debugオフ
na-trium-144 Apr 19, 2026
36a1128
global-errorページのスタイル修正 & フォームパラメータ修正
na-trium-144 Apr 19, 2026
c95258b
sentryの環境変数をREADMEに書いておく
na-trium-144 Apr 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
SENTRY_ORG=
SENTRY_PROJECT=
SENTRY_URL=
SENTRY_AUTH_TOKEN=
SENTRY_DSN=
```

* チャット用にGeminiのAPIキーまたはOpenRouterのAPIキーのいずれかが必要です。未設定の場合チャットが使えません
Expand Down
17 changes: 2 additions & 15 deletions app/(docs)/@chat/error.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client"; // Error boundaries must be Client Components

import clsx from "clsx";
import { ChatAreaContainer } from "./chat/[chatId]/chatArea";
import { ErrorMessage } from "@/errorMessage";

export default function Error({
error,
Expand All @@ -12,20 +12,7 @@ export default function Error({
}) {
return (
<ChatAreaContainer chatId={"error"}>
<p>ページの読み込み中にエラーが発生しました。</p>
<pre
className={clsx(
"border-2 border-current/20 mt-4 rounded-box p-4! bg-base-300! text-base-content!",
"max-w-full whitespace-pre-wrap"
)}
>
{error.message}
</pre>
{error.digest && (
<p className="mt-2 text-sm text-base-content/50">
Digest: {error.digest}
</p>
)}
<ErrorMessage error={error} />
</ChatAreaContainer>
);
}
18 changes: 14 additions & 4 deletions app/(docs)/@docs/[lang]/[pageId]/chatForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
"use client";

import { useState, FormEvent, useEffect, useRef, useCallback, useMemo } from "react";
import {
useState,
FormEvent,
useEffect,
useRef,
useCallback,
useMemo,
} from "react";
// import useSWR from "swr";
// import {
// getQuestionExample,
Expand All @@ -13,6 +20,7 @@ import { usePathname, useRouter } from "next/navigation";
import { ChatStreamEvent } from "@/api/chat/route";
import { useStreamingChatContext } from "@/(docs)/streamingChatContext";
import { revalidateChatAction } from "@/actions/revalidateChat";
import { captureException } from "@sentry/nextjs";

interface ChatFormProps {
path: PagePath;
Expand Down Expand Up @@ -84,7 +92,6 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) {
}, [exampleChoice]);

const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {

let userQuestion = inputValue;
if (!userQuestion && exampleData.length > 0 && exampleChoice) {
// 質問が空欄なら、質問例を使用
Expand Down Expand Up @@ -114,7 +121,8 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) {
execResults,
}),
});
} catch {
} catch (e) {
captureException(e);
setErrorMessage("AIへの接続に失敗しました");
setIsLoading(false);
return;
Expand Down Expand Up @@ -184,12 +192,14 @@ export function ChatForm({ path, sectionContent, close }: ChatFormProps) {
streamingChatContext.finishStreaming();
router.refresh();
}
} catch {
} catch (e) {
captureException(e);
// ignore JSON parse errors
}
}
}
} catch (err) {
captureException(err);
console.error("Stream reading failed:", err);
// ナビゲーション後のエラーはストリーミングを終了してローディングを止める
if (!navigated) {
Expand Down
2 changes: 1 addition & 1 deletion app/(docs)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function WorkspaceLayout({
return (
<StreamingChatProvider>
<ChatAreaStateProvider>
<div className="w-full flex flex-row">
<div className="flex-1 w-full flex flex-row">
{docs}

{chat}
Expand Down
10 changes: 2 additions & 8 deletions app/about/license/ThirdPartyLicenses.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

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

export interface LicenseEntry {
Expand Down Expand Up @@ -80,12 +79,7 @@ export function ThirdPartyLicenses({ licenses }: { licenses: LicenseEntry[] }) {
</p>
)}
{pkg.licenseText && (
<StyledSyntaxHighlighter
className="text-sm"
language={langConstants(undefined)}
>
{pkg.licenseText}
</StyledSyntaxHighlighter>
<FallbackPre className="text-sm">{pkg.licenseText}</FallbackPre>
)}
</div>
</div>
Expand Down
45 changes: 29 additions & 16 deletions app/actions/clearUserCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,39 @@ import { initContext, cacheKeyForPage } from "@/lib/chatHistory";
import { updateTag } from "next/cache";
import { getPagesList } from "@/lib/docs";
import { isCloudflare } from "@/lib/detectCloudflare";
import { headers } from "next/headers";
import { withServerActionInstrumentation } from "@sentry/nextjs";

export async function clearUserCacheAction() {
const ctx = await initContext();
if (!ctx.userId) return;
return withServerActionInstrumentation(
"clearUserCacheAction", // Action name for Sentry
{
headers: await headers(), // Connect client and server traces
recordResponse: true, // Include response data
},
async () => {
const ctx = await initContext();
if (!ctx.userId) return;

const pagesList = await getPagesList();
for (const lang of pagesList) {
for (const page of lang.pages) {
updateTag(cacheKeyForPage({ lang: lang.id, page: page.slug }, ctx.userId));
}
}
const pagesList = await getPagesList();
for (const lang of pagesList) {
for (const page of lang.pages) {
updateTag(
cacheKeyForPage({ lang: lang.id, page: page.slug }, ctx.userId)
);
}
}

if (isCloudflare()) {
const cache = await caches.open("chatHistory");
for (const lang of pagesList) {
for (const page of lang.pages) {
await cache.delete(
cacheKeyForPage({ lang: lang.id, page: page.slug }, ctx.userId)
);
if (isCloudflare()) {
const cache = await caches.open("chatHistory");
for (const lang of pagesList) {
for (const page of lang.pages) {
await cache.delete(
cacheKeyForPage({ lang: lang.id, page: page.slug }, ctx.userId)
);
}
}
}
}
}
);
}
35 changes: 23 additions & 12 deletions app/actions/deleteChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,30 @@ import { z } from "zod";
import { deleteChat, initContext, revalidateChat } from "@/lib/chatHistory";
import { section } from "@/schema/chat";
import { eq } from "drizzle-orm";
import { withServerActionInstrumentation } from "@sentry/nextjs";
import { headers } from "next/headers";

export async function deleteChatAction(chatId: string) {
chatId = z.uuid().parse(chatId);
const ctx = await initContext();
if (!ctx.userId) {
throw new Error("Not authenticated");
}
const deletedChat = await deleteChat(chatId, ctx);
return withServerActionInstrumentation(
"deleteChatAction", // Action name for Sentry
{
headers: await headers(), // Connect client and server traces
recordResponse: true, // Include response data
},
async () => {
chatId = z.uuid().parse(chatId);
const ctx = await initContext();
if (!ctx.userId) {
throw new Error("Not authenticated");
}
const deletedChat = await deleteChat(chatId, ctx);

const targetSection = await ctx.drizzle.query.section.findFirst({
where: eq(section.sectionId, deletedChat[0].sectionId),
});
if (targetSection) {
await revalidateChat(chatId, ctx.userId, targetSection.pagePath);
}
const targetSection = await ctx.drizzle.query.section.findFirst({
where: eq(section.sectionId, deletedChat[0].sectionId),
});
if (targetSection) {
await revalidateChat(chatId, ctx.userId, targetSection.pagePath);
}
}
);
}
61 changes: 37 additions & 24 deletions app/actions/getRedirectFromChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,44 @@ import { initContext } from "@/lib/chatHistory";
import { LangId, PageSlug } from "@/lib/docs";
import { chat, section } from "@/schema/chat";
import { and, eq } from "drizzle-orm";
import { setExtra, withServerActionInstrumentation } from "@sentry/nextjs";
import { headers } from "next/headers";

export async function getRedirectFromChat(chatId: string): Promise<string> {
chatId = z.uuid().parse(chatId);

const { drizzle, userId } = await initContext();
if (!userId) {
throw new Error("Not authenticated");
}

const chatData = (await drizzle.query.chat.findFirst({
where: and(eq(chat.chatId, chatId), eq(chat.userId, userId)),
with: {
section: true,
return withServerActionInstrumentation(
"getRedirectFromChat", // Action name for Sentry
{
headers: await headers(), // Connect client and server traces
recordResponse: true, // Include response data
},
})) as
| (typeof chat.$inferSelect & {
section: typeof section.$inferSelect;
})
| undefined;
if (!chatData?.section) {
throw new Error("Chat or section not found");
}
const [lang, page] = (chatData.section.pagePath.split("/") ?? []) as [
LangId,
PageSlug,
];
return `/${lang}/${page}#${chatData.sectionId}`;
async () => {
setExtra("args", { chatId });

chatId = z.uuid().parse(chatId);

const { drizzle, userId } = await initContext();
if (!userId) {
throw new Error("Not authenticated");
}

const chatData = (await drizzle.query.chat.findFirst({
where: and(eq(chat.chatId, chatId), eq(chat.userId, userId)),
with: {
section: true,
},
})) as
| (typeof chat.$inferSelect & {
section: typeof section.$inferSelect;
})
| undefined;
if (!chatData?.section) {
throw new Error("Chat or section not found");
}
const [lang, page] = (chatData.section.pagePath.split("/") ?? []) as [
LangId,
PageSlug,
];
return `/${lang}/${page}#${chatData.sectionId}`;
}
);
}
41 changes: 27 additions & 14 deletions app/actions/revalidateChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,38 @@

import { initContext, revalidateChat } from "@/lib/chatHistory";
import { PagePath, PagePathSchema } from "@/lib/docs";
import { setExtra, withServerActionInstrumentation } from "@sentry/nextjs";
import { headers } from "next/headers";
import { z } from "zod";

export async function revalidateChatAction(
chatId: string,
pagePath: string | PagePath
) {
chatId = z.uuid().parse(chatId);
if (typeof pagePath === "string") {
if (!/^[a-z0-9_-]+\/[a-z0-9_-]+$/.test(pagePath)) {
throw new Error("Invalid pagePath format");
return withServerActionInstrumentation(
"revalidateChatAction", // Action name for Sentry
{
headers: await headers(), // Connect client and server traces
recordResponse: true, // Include response data
},
async () => {
setExtra("args", { chatId, pagePath });

chatId = z.uuid().parse(chatId);
if (typeof pagePath === "string") {
if (!/^[a-z0-9_-]+\/[a-z0-9_-]+$/.test(pagePath)) {
throw new Error("Invalid pagePath format");
}
const [lang, page] = pagePath.split("/");
pagePath = PagePathSchema.parse({ lang, page });
} else {
pagePath = PagePathSchema.parse(pagePath);
}
const ctx = await initContext();
if (!ctx.userId) {
throw new Error("Not authenticated");
}
await revalidateChat(chatId, ctx.userId, pagePath);
}
const [lang, page] = pagePath.split("/");
pagePath = PagePathSchema.parse({ lang, page });
} else {
pagePath = PagePathSchema.parse(pagePath);
}
const ctx = await initContext();
if (!ctx.userId) {
throw new Error("Not authenticated");
}
await revalidateChat(chatId, ctx.userId, pagePath);
);
}
Loading
Loading