rusty-replay 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/.eslintrc.js +10 -0
  2. package/.vscode/settings.json +3 -0
  3. package/README.md +92 -0
  4. package/apps/web/README.md +11 -0
  5. package/apps/web/api/auth/keys.ts +3 -0
  6. package/apps/web/api/auth/types.ts +25 -0
  7. package/apps/web/api/auth/use-query-profile.ts +19 -0
  8. package/apps/web/api/auth/use-sign-in.ts +24 -0
  9. package/apps/web/api/axios.ts +122 -0
  10. package/apps/web/api/error-code.ts +36 -0
  11. package/apps/web/api/event/keys.ts +14 -0
  12. package/apps/web/api/event/types.ts +91 -0
  13. package/apps/web/api/event/use-mutation-event-assignee.ts +103 -0
  14. package/apps/web/api/event/use-mutation-event-priority.ts +97 -0
  15. package/apps/web/api/event/use-mutation-event-status.ts +198 -0
  16. package/apps/web/api/event/use-query-event-detail.ts +25 -0
  17. package/apps/web/api/event/use-query-event-list.ts +42 -0
  18. package/apps/web/api/health-check/index.ts +21 -0
  19. package/apps/web/api/project/keys.ts +4 -0
  20. package/apps/web/api/project/types.ts +28 -0
  21. package/apps/web/api/project/use-create-project.ts +30 -0
  22. package/apps/web/api/project/use-query-project-list.ts +19 -0
  23. package/apps/web/api/project/use-query-project-users.ts +23 -0
  24. package/apps/web/api/types.ts +44 -0
  25. package/apps/web/app/(auth)/layout.tsx +5 -0
  26. package/apps/web/app/(auth)/sign-in/page.tsx +20 -0
  27. package/apps/web/app/(auth)/sign-up/page.tsx +5 -0
  28. package/apps/web/app/(project)/project/[project_id]/issues/[issue_id]/page.tsx +17 -0
  29. package/apps/web/app/(project)/project/[project_id]/issues/page.tsx +15 -0
  30. package/apps/web/app/(project)/project/[project_id]/page.tsx +10 -0
  31. package/apps/web/app/(project)/project/page.tsx +10 -0
  32. package/apps/web/app/(report)/error-list/page.tsx +7 -0
  33. package/apps/web/app/favicon.ico +0 -0
  34. package/apps/web/app/layout.tsx +35 -0
  35. package/apps/web/app/page.tsx +3 -0
  36. package/apps/web/components/.gitkeep +0 -0
  37. package/apps/web/components/event-list/event-detail.tsx +242 -0
  38. package/apps/web/components/event-list/event-list.tsx +376 -0
  39. package/apps/web/components/event-list/preview.tsx +573 -0
  40. package/apps/web/components/layouts/default-layout.tsx +59 -0
  41. package/apps/web/components/login-form.tsx +124 -0
  42. package/apps/web/components/project/create-project.tsx +130 -0
  43. package/apps/web/components/project/hooks/use-get-event-params.ts +9 -0
  44. package/apps/web/components/project/hooks/use-get-project-params.ts +10 -0
  45. package/apps/web/components/project/project-detail.tsx +240 -0
  46. package/apps/web/components/project/project-list.tsx +137 -0
  47. package/apps/web/components/providers.tsx +25 -0
  48. package/apps/web/components/ui/assignee-dropdown.tsx +176 -0
  49. package/apps/web/components/ui/event-status-dropdown.tsx +104 -0
  50. package/apps/web/components/ui/priority-dropdown.tsx +123 -0
  51. package/apps/web/components/widget/app-sidebar.tsx +225 -0
  52. package/apps/web/components/widget/nav-main.tsx +73 -0
  53. package/apps/web/components/widget/nav-projects.tsx +84 -0
  54. package/apps/web/components/widget/nav-user.tsx +113 -0
  55. package/apps/web/components.json +20 -0
  56. package/apps/web/constants/routes.ts +12 -0
  57. package/apps/web/eslint.config.js +4 -0
  58. package/apps/web/hooks/use-boolean-state.ts +13 -0
  59. package/apps/web/lib/.gitkeep +0 -0
  60. package/apps/web/next-env.d.ts +5 -0
  61. package/apps/web/next.config.mjs +6 -0
  62. package/apps/web/package.json +60 -0
  63. package/apps/web/postcss.config.mjs +1 -0
  64. package/apps/web/providers/flag-provider.tsx +35 -0
  65. package/apps/web/providers/query-client-provider.tsx +17 -0
  66. package/apps/web/providers/telemetry-provider.tsx +12 -0
  67. package/apps/web/tsconfig.json +24 -0
  68. package/apps/web/utils/avatar.ts +26 -0
  69. package/apps/web/utils/date.ts +26 -0
  70. package/apps/web/utils/front-end-tracer.ts +119 -0
  71. package/apps/web/utils/schema/project.schema.ts +12 -0
  72. package/apps/web/utils/span-processor.ts +36 -0
  73. package/package.json +21 -0
  74. package/packages/eslint-config/README.md +3 -0
  75. package/packages/eslint-config/base.js +32 -0
  76. package/packages/eslint-config/next.js +51 -0
  77. package/packages/eslint-config/package.json +25 -0
  78. package/packages/eslint-config/react-internal.js +41 -0
  79. package/packages/rusty-replay/README.md +165 -0
  80. package/packages/rusty-replay/package.json +67 -0
  81. package/packages/rusty-replay/src/environment.ts +27 -0
  82. package/packages/rusty-replay/src/error-batcher.ts +75 -0
  83. package/packages/rusty-replay/src/front-end-tracer.ts +86 -0
  84. package/packages/rusty-replay/src/handler.ts +37 -0
  85. package/packages/rusty-replay/src/index.ts +8 -0
  86. package/packages/rusty-replay/src/recorder.ts +71 -0
  87. package/packages/rusty-replay/src/reporter.ts +115 -0
  88. package/packages/rusty-replay/src/utils.ts +13 -0
  89. package/packages/rusty-replay/tsconfig.build.json +13 -0
  90. package/packages/rusty-replay/tsconfig.json +27 -0
  91. package/packages/rusty-replay/tsup.config.ts +39 -0
  92. package/packages/typescript-config/README.md +3 -0
  93. package/packages/typescript-config/base.json +20 -0
  94. package/packages/typescript-config/nextjs.json +13 -0
  95. package/packages/typescript-config/package.json +9 -0
  96. package/packages/typescript-config/react-library.json +8 -0
  97. package/packages/ui/components.json +20 -0
  98. package/packages/ui/eslint.config.js +4 -0
  99. package/packages/ui/package.json +60 -0
  100. package/packages/ui/postcss.config.mjs +6 -0
  101. package/packages/ui/src/components/.gitkeep +0 -0
  102. package/packages/ui/src/components/avatar.tsx +53 -0
  103. package/packages/ui/src/components/badge.tsx +46 -0
  104. package/packages/ui/src/components/breadcrumb.tsx +109 -0
  105. package/packages/ui/src/components/button.tsx +59 -0
  106. package/packages/ui/src/components/calendar.tsx +75 -0
  107. package/packages/ui/src/components/calendars/date-picker.tsx +43 -0
  108. package/packages/ui/src/components/calendars/date-range-picker.tsx +79 -0
  109. package/packages/ui/src/components/card.tsx +92 -0
  110. package/packages/ui/src/components/checkbox.tsx +32 -0
  111. package/packages/ui/src/components/collapsible.tsx +33 -0
  112. package/packages/ui/src/components/dialog.tsx +135 -0
  113. package/packages/ui/src/components/dialogs/confirmation-modal.tsx +216 -0
  114. package/packages/ui/src/components/dropdown-menu.tsx +261 -0
  115. package/packages/ui/src/components/input.tsx +30 -0
  116. package/packages/ui/src/components/label.tsx +24 -0
  117. package/packages/ui/src/components/login-form.tsx +68 -0
  118. package/packages/ui/src/components/mode-switcher.tsx +34 -0
  119. package/packages/ui/src/components/popover.tsx +48 -0
  120. package/packages/ui/src/components/scroll-area.tsx +58 -0
  121. package/packages/ui/src/components/select.tsx +185 -0
  122. package/packages/ui/src/components/separator.tsx +28 -0
  123. package/packages/ui/src/components/sheet.tsx +139 -0
  124. package/packages/ui/src/components/sidebar.tsx +726 -0
  125. package/packages/ui/src/components/skeleton.tsx +13 -0
  126. package/packages/ui/src/components/sonner.tsx +25 -0
  127. package/packages/ui/src/components/table.tsx +116 -0
  128. package/packages/ui/src/components/tabs.tsx +66 -0
  129. package/packages/ui/src/components/team-switcher.tsx +91 -0
  130. package/packages/ui/src/components/textarea.tsx +18 -0
  131. package/packages/ui/src/components/tooltip.tsx +61 -0
  132. package/packages/ui/src/hooks/.gitkeep +0 -0
  133. package/packages/ui/src/hooks/use-meta-color.ts +28 -0
  134. package/packages/ui/src/hooks/use-mobile.ts +19 -0
  135. package/packages/ui/src/lib/utils.ts +6 -0
  136. package/packages/ui/src/styles/globals.css +138 -0
  137. package/packages/ui/tsconfig.json +13 -0
  138. package/packages/ui/tsconfig.lint.json +8 -0
  139. package/pnpm-workspace.yaml +4 -0
  140. package/tsconfig.json +4 -0
  141. package/turbo.json +21 -0
@@ -0,0 +1,573 @@
1
+ import React, { RefObject } from 'react';
2
+ import {
3
+ Card,
4
+ CardContent,
5
+ CardDescription,
6
+ CardFooter,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from '@workspace/ui/components/card';
10
+ import { Button } from '@workspace/ui/components/button';
11
+ import { Skeleton } from '@workspace/ui/components/skeleton';
12
+ import { Badge } from '@workspace/ui/components/badge';
13
+ import { Separator } from '@workspace/ui/components/separator';
14
+ import { ScrollArea } from '@workspace/ui/components/scroll-area';
15
+ import {
16
+ Table,
17
+ TableBody,
18
+ TableCell,
19
+ TableRow,
20
+ } from '@workspace/ui/components/table';
21
+ import {
22
+ Collapsible,
23
+ CollapsibleContent,
24
+ CollapsibleTrigger,
25
+ } from '@workspace/ui/components/collapsible';
26
+ import {
27
+ ArrowLeft,
28
+ Calendar,
29
+ Globe,
30
+ Smartphone,
31
+ Video,
32
+ AlertCircle,
33
+ Clock,
34
+ AlertTriangle,
35
+ FileText,
36
+ Globe2,
37
+ Send,
38
+ CornerDownRight,
39
+ UserCircle,
40
+ } from 'lucide-react';
41
+ import dayjs from 'dayjs';
42
+ import { AdditionalInfo } from '@workspace/rusty-replay/index';
43
+ import { EventReportResponse } from '@/api/event/types';
44
+ import { formatDate } from '@/utils/date';
45
+ import { useQueryProjectUsers } from '@/api/project/use-query-project-users';
46
+ import { PriorityDropdown } from '../ui/priority-dropdown';
47
+ import { AssigneeDropdown } from '../ui/assignee-dropdown';
48
+ import { EventStatusDropdown } from '../ui/event-status-dropdown';
49
+
50
+ export interface BackButtonProps {
51
+ onClick: VoidFunction;
52
+ }
53
+
54
+ export const BackButton = ({ onClick }: BackButtonProps) => (
55
+ <Button variant="outline" size="sm" onClick={onClick}>
56
+ <ArrowLeft size={16} className="mr-2" />
57
+ 에러 목록으로 돌아가기
58
+ </Button>
59
+ );
60
+
61
+ export const LoadingSkeleton = () => (
62
+ <Card>
63
+ <CardHeader>
64
+ <Skeleton className="h-8 w-1/3" />
65
+ <Skeleton className="h-4 w-1/4" />
66
+ </CardHeader>
67
+ <CardContent>
68
+ <div className="space-y-4">
69
+ <Skeleton className="h-4 w-full" />
70
+ <Skeleton className="h-4 w-full" />
71
+ <Skeleton className="h-4 w-3/4" />
72
+ </div>
73
+ </CardContent>
74
+ </Card>
75
+ );
76
+
77
+ export interface ErrorNotFoundProps {
78
+ eventId: number | undefined;
79
+ goBack: VoidFunction;
80
+ }
81
+
82
+ export const EventNotFound = ({ eventId, goBack }: ErrorNotFoundProps) => (
83
+ <Card>
84
+ <CardContent className="p-6 flex flex-col items-center justify-center">
85
+ <AlertCircle size={48} className="text-red-500 mb-4" />
86
+ <h2 className="text-xl font-semibold mb-2">에러를 찾을 수 없습니다</h2>
87
+ <p className="text-muted-foreground text-center">
88
+ 요청하신 이벤트 ID: {eventId}를 찾을 수 없습니다.
89
+ <br />
90
+ 이벤트가 삭제되었거나 접근 권한이 없을 수 있습니다.
91
+ </p>
92
+ <Button variant="outline" className="mt-4" onClick={goBack}>
93
+ 이벤트 목록으로 돌아가기
94
+ </Button>
95
+ </CardContent>
96
+ </Card>
97
+ );
98
+
99
+ export interface BasicInfoItemProps {
100
+ label: string;
101
+ children: React.ReactNode;
102
+ }
103
+
104
+ export const BasicInfoItem = ({ label, children }: BasicInfoItemProps) => (
105
+ <div>
106
+ <h3 className="text-sm font-medium text-muted-foreground mb-1">{label}</h3>
107
+ {children}
108
+ </div>
109
+ );
110
+
111
+ export interface StacktracePreviewProps {
112
+ stacktrace: string;
113
+ }
114
+
115
+ export const StacktracePreview = ({ stacktrace }: StacktracePreviewProps) => (
116
+ <div>
117
+ <h3 className="text-md font-semibold mb-2">스택트레이스 미리보기</h3>
118
+ <div className="bg-gray-100 p-3 rounded-md overflow-auto max-h-32">
119
+ <pre className="text-xs">
120
+ {stacktrace.split('\n').slice(0, 5).join('\n')}
121
+ {stacktrace.split('\n').length > 5 && '\n...'}
122
+ </pre>
123
+ </div>
124
+ </div>
125
+ );
126
+
127
+ export interface AdditionalInfoSectionProps {
128
+ additionalInfo: AdditionalInfo | null;
129
+ }
130
+
131
+ export const AdditionalInfoSection = ({
132
+ additionalInfo,
133
+ }: AdditionalInfoSectionProps) => {
134
+ if (!additionalInfo) return null;
135
+
136
+ return (
137
+ <div>
138
+ <h3 className="text-md font-semibold mb-2 flex items-center">
139
+ <FileText size={16} className="mr-2" />
140
+ 추가 정보
141
+ </h3>
142
+
143
+ <div className="space-y-4">
144
+ {/* 페이지 URL */}
145
+ {additionalInfo.pageUrl && (
146
+ <div>
147
+ <h4 className="text-sm font-medium text-muted-foreground mb-1 flex items-center">
148
+ <Globe2 size={14} className="mr-1" />
149
+ 페이지 URL
150
+ </h4>
151
+ <div className="bg-gray-50 p-2 rounded text-sm break-all">
152
+ {additionalInfo.pageUrl}
153
+ </div>
154
+ </div>
155
+ )}
156
+
157
+ {/* 요청 정보 */}
158
+ {additionalInfo.request && (
159
+ <Collapsible className="w-full">
160
+ <div className="flex items-center justify-between">
161
+ <h4 className="text-sm font-medium text-muted-foreground mb-1 flex items-center">
162
+ <Send size={14} className="mr-1" />
163
+ 요청 정보
164
+ </h4>
165
+ <CollapsibleTrigger asChild>
166
+ <Button variant="ghost" size="sm">
167
+ 자세히 보기
168
+ </Button>
169
+ </CollapsibleTrigger>
170
+ </div>
171
+
172
+ <div className="bg-gray-50 p-3 rounded overflow-auto">
173
+ <div className="flex gap-2 mb-1">
174
+ <Badge
175
+ variant="outline"
176
+ className="font-medium text-blue-600 bg-blue-50"
177
+ >
178
+ {additionalInfo.request.method}
179
+ </Badge>
180
+ <span className="break-all">{additionalInfo.request.url}</span>
181
+ </div>
182
+
183
+ <CollapsibleContent>
184
+ {Object.keys(additionalInfo.request.headers).length > 0 && (
185
+ <div className="mt-2">
186
+ <h5 className="text-xs font-medium mb-1">Headers:</h5>
187
+ <ScrollArea className="h-32 rounded border p-2">
188
+ <pre className="text-xs">
189
+ {JSON.stringify(
190
+ additionalInfo.request.headers,
191
+ null,
192
+ 2
193
+ )}
194
+ </pre>
195
+ </ScrollArea>
196
+ </div>
197
+ )}
198
+ </CollapsibleContent>
199
+ </div>
200
+ </Collapsible>
201
+ )}
202
+
203
+ {/* 응답 정보 */}
204
+ {additionalInfo.response && (
205
+ <Collapsible className="w-full">
206
+ <div className="flex items-center justify-between">
207
+ <h4 className="text-sm font-medium text-muted-foreground mb-1 flex items-center">
208
+ <CornerDownRight size={14} className="mr-1" />
209
+ 응답 정보
210
+ </h4>
211
+ <CollapsibleTrigger asChild>
212
+ <Button variant="ghost" size="sm">
213
+ 자세히 보기
214
+ </Button>
215
+ </CollapsibleTrigger>
216
+ </div>
217
+
218
+ <div className="bg-gray-50 p-3 rounded overflow-auto">
219
+ <div className="flex gap-2 mb-1">
220
+ <Badge
221
+ variant="outline"
222
+ className={`${
223
+ additionalInfo.response.status >= 400
224
+ ? 'text-red-600 bg-red-50'
225
+ : 'text-green-600 bg-green-50'
226
+ }`}
227
+ >
228
+ {additionalInfo.response.status}
229
+ </Badge>
230
+ <span>{additionalInfo.response.statusText}</span>
231
+ </div>
232
+
233
+ <CollapsibleContent>
234
+ {additionalInfo.response.data && (
235
+ <div className="mt-2">
236
+ <h5 className="text-xs font-medium mb-1">응답 데이터:</h5>
237
+ <div className="bg-gray-100 p-2 rounded">
238
+ <Table>
239
+ <TableBody>
240
+ {additionalInfo.response.data.errorCode && (
241
+ <TableRow>
242
+ <TableCell className="font-medium text-xs py-1">
243
+ 에러 코드
244
+ </TableCell>
245
+ <TableCell className="py-1">
246
+ <code className="bg-gray-200 px-1 rounded text-xs">
247
+ {additionalInfo.response.data.errorCode}
248
+ </code>
249
+ </TableCell>
250
+ </TableRow>
251
+ )}
252
+ {additionalInfo.response.data.message && (
253
+ <TableRow>
254
+ <TableCell className="font-medium text-xs py-1">
255
+ 메시지
256
+ </TableCell>
257
+ <TableCell className="text-xs py-1">
258
+ {additionalInfo.response.data.message}
259
+ </TableCell>
260
+ </TableRow>
261
+ )}
262
+ </TableBody>
263
+ </Table>
264
+ </div>
265
+ </div>
266
+ )}
267
+ </CollapsibleContent>
268
+ </div>
269
+ </Collapsible>
270
+ )}
271
+ </div>
272
+ </div>
273
+ );
274
+ };
275
+
276
+ export interface OverviewTabProps {
277
+ error: EventReportResponse;
278
+ formatStacktrace: (stacktrace: string) => string;
279
+ hasReplay: boolean;
280
+ setActiveTab: (tab: string) => void;
281
+ projectId: number | undefined;
282
+ }
283
+
284
+ export const OverviewTab = ({
285
+ error,
286
+ formatStacktrace,
287
+ hasReplay,
288
+ setActiveTab,
289
+ projectId,
290
+ }: OverviewTabProps) => {
291
+ const { data: userList } = useQueryProjectUsers({ projectId: projectId! });
292
+
293
+ return (
294
+ <Card>
295
+ <CardHeader>
296
+ <div className="flex justify-between items-start">
297
+ <div>
298
+ <CardTitle className="text-xl font-bold text-red-600 flex items-center gap-2">
299
+ <AlertCircle size={20} />
300
+ {error.message}
301
+ </CardTitle>
302
+ <CardDescription>
303
+ Error ID: {error.id} | Group Hash: {error.groupHash}
304
+ </CardDescription>
305
+ </div>
306
+
307
+ <div className="flex items-center gap-6">
308
+ <div className="flex items-center gap-2">
309
+ <span className="text-xs font-medium text-muted-foreground">
310
+ Status
311
+ </span>
312
+ <EventStatusDropdown
313
+ status={error.status}
314
+ projectId={projectId}
315
+ eventId={error.id}
316
+ />
317
+ </div>
318
+
319
+ <div className="flex items-center gap-2">
320
+ <span className="text-xs font-medium text-muted-foreground">
321
+ Priority
322
+ </span>
323
+ <PriorityDropdown
324
+ priority={error.priority}
325
+ projectId={projectId}
326
+ eventId={error.id}
327
+ />
328
+ </div>
329
+
330
+ <div className="flex items-center gap-2">
331
+ <span className="text-xs font-medium text-muted-foreground">
332
+ Assignee
333
+ </span>
334
+ <AssigneeDropdown
335
+ projectId={projectId}
336
+ eventId={error.id}
337
+ userList={userList}
338
+ currentAssigneeId={error.assignedTo}
339
+ />
340
+ </div>
341
+ </div>
342
+ </div>
343
+ </CardHeader>
344
+
345
+ <CardContent className="space-y-6">
346
+ {/* <div className="bg-gray-50 p-4 rounded-lg border border-gray-200">
347
+ <h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
348
+ <UserCircle size={16} />
349
+ 이슈 관리
350
+ </h3>
351
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
352
+ <div>
353
+ <h4 className="text-sm font-medium text-muted-foreground mb-1">
354
+ 우선순위
355
+ </h4>
356
+ <PriorityDropdown
357
+ priority={error.priority}
358
+ projectId={projectId}
359
+ eventId={error.id}
360
+ />
361
+ </div>
362
+ <div>
363
+ <h4 className="text-sm font-medium text-muted-foreground mb-1">
364
+ 담당자
365
+ </h4>
366
+ <AssigneeDropdown
367
+ projectId={projectId}
368
+ eventId={error.id}
369
+ userList={userList}
370
+ currentAssigneeId={error.assignedTo}
371
+ />
372
+ </div>
373
+ </div>
374
+ </div> */}
375
+
376
+ <Separator />
377
+
378
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
379
+ <BasicInfoItem label="발생 시간">
380
+ <div className="flex items-center gap-2">
381
+ <Calendar size={16} />
382
+ <span>{formatDate(error.timestamp)}</span>
383
+ </div>
384
+ </BasicInfoItem>
385
+
386
+ <BasicInfoItem label="앱 버전">
387
+ <Badge variant="outline">{error.appVersion}</Badge>
388
+ </BasicInfoItem>
389
+
390
+ {error.browser && (
391
+ <BasicInfoItem label="브라우저">
392
+ <div className="flex items-center gap-2">
393
+ <Globe size={16} />
394
+ <span>{error.browser}</span>
395
+ </div>
396
+ </BasicInfoItem>
397
+ )}
398
+
399
+ {error.os && (
400
+ <BasicInfoItem label="운영체제">
401
+ <div className="flex items-center gap-2">
402
+ <Smartphone size={16} />
403
+ <span>{error.os}</span>
404
+ </div>
405
+ </BasicInfoItem>
406
+ )}
407
+
408
+ <BasicInfoItem label="이슈 ID">
409
+ <Badge variant="secondary">{error.issueId}</Badge>
410
+ </BasicInfoItem>
411
+
412
+ <BasicInfoItem label="세션 리플레이">
413
+ {hasReplay ? (
414
+ <Badge variant="outline" className="bg-green-100 text-green-800">
415
+ 사용 가능 ({error.replay.length}개 이벤트)
416
+ </Badge>
417
+ ) : (
418
+ <Badge variant="outline" className="text-muted-foreground">
419
+ 없음
420
+ </Badge>
421
+ )}
422
+ </BasicInfoItem>
423
+ </div>
424
+
425
+ <Separator />
426
+
427
+ <AdditionalInfoSection additionalInfo={error.additionalInfo} />
428
+
429
+ <StacktracePreview stacktrace={error.stacktrace} />
430
+ </CardContent>
431
+
432
+ <CardFooter className="flex justify-between">
433
+ <Button
434
+ variant="outline"
435
+ size="sm"
436
+ onClick={() => window.history.back()}
437
+ >
438
+ 뒤로 가기
439
+ </Button>
440
+ {hasReplay && (
441
+ <Button
442
+ variant="default"
443
+ size="sm"
444
+ className="flex items-center gap-2"
445
+ onClick={() => setActiveTab('replay')}
446
+ >
447
+ <Video size={16} />
448
+ 세션 리플레이 보기
449
+ </Button>
450
+ )}
451
+ </CardFooter>
452
+ </Card>
453
+ );
454
+ };
455
+
456
+ export interface StacktraceTabProps {
457
+ error: EventReportResponse;
458
+ formatStacktrace: (stacktrace: string) => string;
459
+ }
460
+
461
+ export const StacktraceTab = ({
462
+ error,
463
+ formatStacktrace,
464
+ }: StacktraceTabProps) => (
465
+ <Card>
466
+ <CardHeader>
467
+ <CardTitle className="text-xl">스택트레이스</CardTitle>
468
+ <CardDescription>
469
+ 에러가 발생한 위치와 호출 스택을 확인할 수 있습니다.
470
+ </CardDescription>
471
+ </CardHeader>
472
+
473
+ <CardContent>
474
+ <ScrollArea className="bg-gray-100 p-4 rounded-md h-[500px]">
475
+ <pre
476
+ className="text-xs leading-relaxed whitespace-pre-wrap"
477
+ dangerouslySetInnerHTML={{
478
+ __html: formatStacktrace(error.stacktrace),
479
+ }}
480
+ />
481
+ </ScrollArea>
482
+ </CardContent>
483
+ </Card>
484
+ );
485
+
486
+ export interface ReplayTabProps {
487
+ error: EventReportResponse;
488
+ playerRef: RefObject<HTMLDivElement | null>;
489
+ playerInitialized: boolean;
490
+ playerError: string | null;
491
+ setPlayerError: (error: string | null) => void;
492
+ setPlayerInitialized: (initialized: boolean) => void;
493
+ setActiveTab: (tab: string) => void;
494
+ }
495
+
496
+ export const ReplayTab = ({
497
+ error,
498
+ playerRef,
499
+ playerInitialized,
500
+ playerError,
501
+ setPlayerError,
502
+ setPlayerInitialized,
503
+ setActiveTab,
504
+ }: ReplayTabProps) => (
505
+ <Card>
506
+ <CardHeader>
507
+ <CardTitle className="text-xl">세션 리플레이</CardTitle>
508
+ <CardDescription>
509
+ 에러 발생 전 사용자의 행동을 녹화한 영상입니다.
510
+ </CardDescription>
511
+ </CardHeader>
512
+
513
+ <CardContent>
514
+ {/* 플레이어 오류 표시 */}
515
+ {playerError && (
516
+ <div className="bg-red-50 border border-red-200 rounded-md p-4 mb-4">
517
+ <div className="flex items-start">
518
+ <AlertTriangle className="text-red-500 mr-2 mt-0.5" size={18} />
519
+ <div>
520
+ <h3 className="font-medium text-red-800">리플레이 로드 오류</h3>
521
+ <p className="text-sm text-red-700 mt-1">{playerError}</p>
522
+ <pre className="mt-2 text-xs bg-white p-2 rounded overflow-auto max-h-32">
523
+ {JSON.stringify(error.replay?.slice(0, 2), null, 2)}
524
+ </pre>
525
+ <Button
526
+ variant="outline"
527
+ size="sm"
528
+ className="mt-2"
529
+ onClick={() => {
530
+ setPlayerError(null);
531
+ setPlayerInitialized(false);
532
+ setActiveTab('replay');
533
+ }}
534
+ >
535
+ 다시 시도
536
+ </Button>
537
+ </div>
538
+ </div>
539
+ </div>
540
+ )}
541
+
542
+ <div
543
+ id="player"
544
+ ref={playerRef}
545
+ className="rrweb-player w-full overflow-hidden"
546
+ >
547
+ {!playerInitialized && !playerError && (
548
+ <div className="w-full h-96 bg-gray-100 flex items-center justify-center">
549
+ <Skeleton className="h-full w-full" />
550
+ </div>
551
+ )}
552
+ </div>
553
+ </CardContent>
554
+
555
+ <CardFooter className="text-sm text-muted-foreground">
556
+ {error.replay &&
557
+ Array.isArray(error.replay) &&
558
+ error.replay.length > 0 && (
559
+ <div className="flex items-center gap-2">
560
+ <Clock size={14} />
561
+ <span>
562
+ {error.replay.length} 이벤트 | 첫 이벤트:{' '}
563
+ {dayjs(error.replay[0]?.timestamp).format('HH:mm:ss')} | 마지막
564
+ 이벤트:{' '}
565
+ {dayjs(error.replay[error.replay.length - 1]?.timestamp).format(
566
+ 'HH:mm:ss'
567
+ )}
568
+ </span>
569
+ </div>
570
+ )}
571
+ </CardFooter>
572
+ </Card>
573
+ );
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import React, { ReactNode } from 'react';
4
+ import {
5
+ Breadcrumb,
6
+ BreadcrumbItem,
7
+ BreadcrumbLink,
8
+ BreadcrumbList,
9
+ BreadcrumbPage,
10
+ BreadcrumbSeparator,
11
+ } from '@workspace/ui/components/breadcrumb';
12
+ import { Separator } from '@workspace/ui/components/separator';
13
+ import {
14
+ SidebarInset,
15
+ SidebarProvider,
16
+ SidebarTrigger,
17
+ } from '@workspace/ui/components/sidebar';
18
+ import { ModeSwitcher } from '@workspace/ui/components/mode-switcher';
19
+ import { AppSidebar } from '../widget/app-sidebar';
20
+
21
+ export default function DefaultLayout({ children }: { children: ReactNode }) {
22
+ return (
23
+ <SidebarProvider>
24
+ <AppSidebar />
25
+ <SidebarInset>
26
+ <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12 justify-between">
27
+ <div className="flex items-center gap-2 px-4">
28
+ <SidebarTrigger className="-ml-1" />
29
+ <Separator orientation="vertical" className="mr-2 h-4" />
30
+ <Breadcrumb>
31
+ <BreadcrumbList>
32
+ <BreadcrumbItem className="hidden md:block">
33
+ <BreadcrumbLink href="#">rusty-replay</BreadcrumbLink>
34
+ </BreadcrumbItem>
35
+ <BreadcrumbSeparator className="hidden md:block" />
36
+ <BreadcrumbItem>
37
+ <BreadcrumbPage>issue tracing</BreadcrumbPage>
38
+ </BreadcrumbItem>
39
+ </BreadcrumbList>
40
+ </Breadcrumb>
41
+ </div>
42
+ <div className="px-4">
43
+ <ModeSwitcher />
44
+ </div>
45
+ </header>
46
+ <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
47
+ {children}
48
+
49
+ {/* <div className="grid auto-rows-min gap-4 md:grid-cols-3">
50
+ <div className="aspect-video rounded-xl bg-muted/50" />
51
+ <div className="aspect-video rounded-xl bg-muted/50" />
52
+ <div className="aspect-video rounded-xl bg-muted/50" />
53
+ </div> */}
54
+ {/* <div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" /> */}
55
+ </div>
56
+ </SidebarInset>
57
+ </SidebarProvider>
58
+ );
59
+ }