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
package/.eslintrc.js ADDED
@@ -0,0 +1,10 @@
1
+ // This configuration only applies to the package manager root.
2
+ /** @type {import("eslint").Linter.Config} */
3
+ module.exports = {
4
+ ignorePatterns: ["apps/**", "packages/**"],
5
+ extends: ["@workspace/eslint-config/library.js"],
6
+ parser: "@typescript-eslint/parser",
7
+ parserOptions: {
8
+ project: true,
9
+ },
10
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "typescript.tsdk": "node_modules/typescript/lib"
3
+ }
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # rusty replay
2
+
3
+ **rusty replay**는 Sentry와 같은 제한된 세션 리플레이를 해결하기 위해 개발된 오류 추적 및 세션 리플레이 솔루션입니다. 무제한 세션 캡처와 데이터 소유권을 가진 채 비용을 크게 절감할 수 있습니다.
4
+
5
+ ## 주요 기능
6
+
7
+ ### 오류 추적
8
+
9
+ - 자동 전역 오류 처리 (`window.onerror`, `unhandledrejection`)
10
+ - 스택 트레이스 캡처 및 소스 매핑
11
+ - 브라우저 및 환경 정보 수집
12
+
13
+ ### 세션 리플레이
14
+
15
+ - 오류 발생 전 최대 10초 동안의 사용자 활동 기록
16
+ - DOM 스냅샷 및 상호작용 캡처
17
+ - 시간별 이벤트 추적
18
+
19
+ ### 대시보드
20
+
21
+ - 다중 프로젝트 관리
22
+ - 이슈 및 이벤트 목록
23
+ - 인터랙티브 세션 리플레이 뷰어
24
+ - 스택 트레이스 분석
25
+
26
+ ## 대시보드 화면
27
+
28
+ ### 프로젝트 상세보기
29
+
30
+ <img width="500" alt="r-1" src="https://github.com/user-attachments/assets/459f4fd2-d074-4fd1-9a09-fe52ca05813f" />
31
+
32
+ - 프로젝트의 모든 오류 이벤트를 종합적으로 볼 수 있는 대시보드
33
+
34
+ ### 프로젝트 이벤트 타임라인
35
+
36
+ <img width="500" alt="r-2" src="https://github.com/user-attachments/assets/98437a01-ebe0-4235-bae3-e9a8b67373d1" />
37
+
38
+ - 프로젝트에서 캡처된 모든 이벤트를 시간순으로 확인
39
+
40
+ ### 이벤트 목록
41
+
42
+ <img width="500" alt="r-3" src="https://github.com/user-attachments/assets/cc86a2ff-73b2-4a80-841c-d1b7952a35bf" />
43
+
44
+ - 모든 이벤트를 한눈에 확인
45
+
46
+ ### 이벤트 상세보기
47
+
48
+ <img width="500" alt="r-4" src="https://github.com/user-attachments/assets/6cd280f1-f62e-4c4a-9eaa-36d63d805e68" />
49
+
50
+ - 이벤트 발생 시간, API 정보, 스택 트레이스 등 자세한 정보를 확인
51
+
52
+ ### 스택 트레이스 분석
53
+
54
+ <img width="500" alt="r-5" src="https://github.com/user-attachments/assets/63252d6f-a0fb-499b-9513-c614f58792d8" />
55
+
56
+ - 오류가 발생한 정확한 위치와 원인을 파악
57
+
58
+ ### 세션 리플레이
59
+
60
+ <img width="500" alt="r-7" src="https://github.com/user-attachments/assets/7fb90cae-1b12-48a9-88e4-7e45cb707091" />
61
+
62
+ - 오류 발생 전 사용자 활동을 재생하여 문제 상황 파악
63
+
64
+ ## 기술 스택
65
+
66
+ - **프론트엔드 SDK**: TypeScript, rrweb
67
+ - **프론트엔드 대시보드**: Next.js v15
68
+ - **백엔드**: Rust, Actix-Web
69
+ - **데이터베이스**: MySQL, SeaORM
70
+
71
+ ### 🔄 flow chart
72
+
73
+ <img width="500" alt="r-4" src="https://github.com/user-attachments/assets/652bebb0-2a5f-48c7-b9d5-bfe0221c7acc" />
74
+
75
+ ```
76
+ rusty-replay/
77
+ ├── apps
78
+ │ └── web # Dashboard (Next.js + rrweb)
79
+ ├── packages
80
+ │ ├── rusty-replay # SDK (TypeScript + rrweb)
81
+ │ └── ui # Shared UI components
82
+ ```
83
+
84
+ ## 🦀 Backend Repository
85
+
86
+ 🔗 **GitHub 저장소**: [rusty-replay/replay-be](https://github.com/rusty-replay/replay-be)
87
+
88
+ ## 🕹️ SDK Repository
89
+
90
+ 📦 NPM 패키지: [`npm i rusty-replay`](https://www.npmjs.com/package/rusty-replay)
91
+
92
+ 📖 [SDK 사용법 바로가기](https://github.com/rusty-replay/replay/tree/main/packages/rusty-replay#readme)
@@ -0,0 +1,11 @@
1
+ ### 🔧 Environment Variables
2
+
3
+ `.env` 파일을 프로젝트 루트에 생성하고 아래 내용을 추가해주세요:
4
+
5
+ ```
6
+ # Back-end end point 연결
7
+ NEXT_PUBLIC_API_URL=http://localhost:8081
8
+
9
+ # 프로젝트 apiKey
10
+ NEXT_PUBLIC_RUSTY_REPLAY_API_KEY=proj_xxx
11
+ ```
@@ -0,0 +1,3 @@
1
+ export const authKeys = {
2
+ profile: () => `/api/auth/me`,
3
+ };
@@ -0,0 +1,25 @@
1
+ export interface SignInRequest {
2
+ email: string;
3
+ password: string;
4
+ }
5
+
6
+ export interface SignInResponse {
7
+ token: string;
8
+ refreshToken: string;
9
+ userId: number;
10
+ username: string;
11
+ email: string;
12
+ role: 'user' | 'admin';
13
+ }
14
+
15
+ export interface RefreshTokenResponse {
16
+ accessToken: string;
17
+ refreshToken: string;
18
+ }
19
+
20
+ export interface UserResponse {
21
+ id: number;
22
+ username: string;
23
+ email: string;
24
+ role: string; // TODO
25
+ }
@@ -0,0 +1,19 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { authKeys } from './keys';
3
+ import axiosInstance from '../axios';
4
+ import { UseQueryCustomOptions } from '../types';
5
+ import { UserResponse } from './types';
6
+
7
+ export function useQueryProfile(
8
+ options?: UseQueryCustomOptions<void, UserResponse>
9
+ ) {
10
+ const queryKey = authKeys.profile();
11
+ const queryFn = async () =>
12
+ await axiosInstance.get(queryKey).then((res) => res.data);
13
+
14
+ return useQuery({
15
+ queryKey: [queryKey],
16
+ queryFn,
17
+ ...options,
18
+ });
19
+ }
@@ -0,0 +1,24 @@
1
+ import { useMutation } from '@tanstack/react-query';
2
+ import { UseMutationCustomOptions } from '../types';
3
+ import { SignInRequest, SignInResponse } from './types';
4
+ import axiosInstance from '../axios';
5
+ import { toast } from '@workspace/ui/components/sonner';
6
+
7
+ export function useSignIn(
8
+ options?: UseMutationCustomOptions<SignInResponse, SignInRequest>
9
+ ) {
10
+ const mutationKey = '/auth/login';
11
+ const mutationFn = async (data: SignInRequest) =>
12
+ await axiosInstance.post(mutationKey, data).then((res) => res.data);
13
+
14
+ return useMutation({
15
+ mutationKey: [mutationKey],
16
+ mutationFn,
17
+ onSuccess: (data) => {
18
+ toast.success('로그인 성공', {
19
+ description: `${data.username}님 환영합니다!`,
20
+ });
21
+ },
22
+ ...options,
23
+ });
24
+ }
@@ -0,0 +1,122 @@
1
+ import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
2
+ import { RefreshTokenResponse } from './auth/types';
3
+ import { ResponseError } from './types';
4
+ import { toast } from '@workspace/ui/components/sonner';
5
+ import { captureException } from 'rusty-replay';
6
+
7
+ interface CustomInternalAxiosRequestConfig extends InternalAxiosRequestConfig {
8
+ _retry?: boolean;
9
+ }
10
+
11
+ const axiosInstance: AxiosInstance = axios.create({
12
+ baseURL: process.env.NEXT_PUBLIC_API_URL,
13
+ withCredentials: true,
14
+ });
15
+
16
+ let isRefreshing = false;
17
+ let refreshSubscribers: ((token: string) => void)[] = [];
18
+
19
+ const subscribeTokenRefresh = (cb: (token: string) => void) => {
20
+ refreshSubscribers.push(cb);
21
+ };
22
+
23
+ const onRefreshed = (token: string) => {
24
+ refreshSubscribers.map((cb) => cb(token));
25
+ refreshSubscribers = [];
26
+ };
27
+
28
+ // 현재 경로가 인증 페이지인지 확인하는 함수
29
+ const isAuthPage = () => {
30
+ if (typeof window === 'undefined') return false;
31
+
32
+ const path = window.location.pathname;
33
+ return path === '/login' || path === '/signup' || path === '/find-password';
34
+ };
35
+
36
+ axiosInstance.interceptors.response.use(
37
+ (response) => response,
38
+ async (error: ResponseError) => {
39
+ const originalRequest = error.config as CustomInternalAxiosRequestConfig;
40
+ console.log('error>>>>>>>>>>>', error);
41
+
42
+ if (!originalRequest) {
43
+ return Promise.reject(error);
44
+ }
45
+
46
+ const errorCode = error.response?.data?.errorCode;
47
+ console.log('errorCode>>>', errorCode);
48
+
49
+ // 인증 관련 에러 처리
50
+ if (
51
+ [
52
+ 'InvalidAuthToken',
53
+ 'InvalidRefreshToken',
54
+ 'ExpiredRefreshToken',
55
+ 'ExpiredAuthToken',
56
+ 'NeedAuthToken',
57
+ ].includes(errorCode!)
58
+ ) {
59
+ // 인증 페이지(로그인, 회원가입 등)에서는 리다이렉션 및 토큰 갱신 시도를 하지 않음
60
+ if (isAuthPage()) {
61
+ return Promise.reject(error);
62
+ }
63
+
64
+ // Refresh 토큰 만료/없음 처리
65
+ if (
66
+ errorCode === 'ExpiredRefreshToken' ||
67
+ errorCode === 'InvalidRefreshToken'
68
+ ) {
69
+ toast.error('세션이 만료되었습니다. 다시 로그인해 주세요.');
70
+
71
+ if (!window.location.pathname.includes('/sign-in')) {
72
+ window.location.href = '/sign-in';
73
+ }
74
+
75
+ return new Promise(() => {});
76
+ }
77
+
78
+ // Access 토큰 만료 처리
79
+ if (
80
+ (errorCode === 'InvalidAuthToken' ||
81
+ errorCode === 'ExpiredAuthToken') &&
82
+ //errorCode === 'NeedAuthToken'
83
+ !originalRequest._retry
84
+ ) {
85
+ if (isRefreshing) {
86
+ return new Promise((resolve) => {
87
+ subscribeTokenRefresh(() => {
88
+ resolve(axiosInstance(originalRequest));
89
+ });
90
+ });
91
+ }
92
+
93
+ originalRequest._retry = true;
94
+ isRefreshing = true;
95
+
96
+ try {
97
+ await axiosInstance.post(
98
+ '/auth/refresh',
99
+ {},
100
+ { withCredentials: true }
101
+ );
102
+
103
+ isRefreshing = false;
104
+ onRefreshed('');
105
+ return axiosInstance(originalRequest);
106
+ } catch (refreshError) {
107
+ isRefreshing = false;
108
+
109
+ if (!isAuthPage()) {
110
+ window.location.href = '/login';
111
+ }
112
+
113
+ return new Promise(() => {});
114
+ }
115
+ }
116
+ }
117
+
118
+ return Promise.reject(error);
119
+ }
120
+ );
121
+
122
+ export default axiosInstance;
@@ -0,0 +1,36 @@
1
+ export const ERROR_CODE = {
2
+ ValidationError: '요청값 유효성 검사에 실패했습니다',
3
+ DuplicateAccountEmail: '이미 등록된 이메일입니다. 로그인해주세요',
4
+ InvalidPassword: '비밀번호는 최소 8자 이상이어야 합니다',
5
+ InvalidEmailPwd: '잘못된 자격 증명입니다',
6
+ NotRefreshToken: '잘못된 리프레시 토큰입니다',
7
+ InvalidRefreshToken: '리프레시 토큰이 유효하지 않습니다',
8
+ InvalidApiKey: '유효하지 않은 API 키입니다',
9
+ AuthenticationFailed: '인증에 실패했습니다',
10
+ ExpiredAuthToken: '로그인 토큰이 만료되었습니다',
11
+ InvalidAuthToken: '유효하지 않은 로그인 토큰입니다',
12
+ NotEnoughPermission: '권한이 부족합니다',
13
+ MemberNotFound: '사용자를 찾을 수 없습니다',
14
+ GroupNotFound: '유효하지 않은 그룹 ID입니다',
15
+ ProjectNotFound: '유효하지 않은 프로젝트 ID입니다',
16
+ ErrorLogNotFound: '유효하지 않은 에러 로그 ID입니다',
17
+ DatabaseError: '데이터베이스 오류가 발생했습니다',
18
+ InternalError: '내부 서버 오류가 발생했습니다',
19
+ TokenGenerationFailed: '토큰 생성에 실패했습니다',
20
+ JwtInvalidToken: 'JWT 토큰이 유효하지 않습니다',
21
+ JwtExpiredToken: 'JWT 토큰이 만료되었습니다',
22
+ ExpiredRefreshToken: 'refreshToken이 만료되었습니다',
23
+ Default: '알 수 없는 오류가 발생했습니다',
24
+ } as const;
25
+
26
+ // 사용 예시:
27
+ export const getErrorMessage = (errorCode: keyof typeof ERROR_CODE) => {
28
+ return ERROR_CODE[errorCode] || ERROR_CODE.Default;
29
+ };
30
+
31
+ export const getError = (errorCode: keyof typeof ERROR_CODE) => {
32
+ return {
33
+ errorCode,
34
+ errorMessage: getErrorMessage(errorCode),
35
+ };
36
+ };
@@ -0,0 +1,14 @@
1
+ export const eventKeys = {
2
+ list: (projectId: number, queryParams?: URLSearchParams) => {
3
+ const baseUrl = `/api/projects/${projectId}/events`;
4
+ if (queryParams && queryParams.toString()) {
5
+ return `${baseUrl}?${queryParams.toString()}`;
6
+ }
7
+ return baseUrl;
8
+ },
9
+ detail: (projectId: number, eventId: number) =>
10
+ `/api/projects/${projectId}/events/${eventId}`,
11
+ priority: (projectId: number) => `/api/projects/${projectId}/events/priority`,
12
+ assignee: (projectId: number) => `/api/projects/${projectId}/events/assignee`,
13
+ status: (projectId: number) => `/api/projects/${projectId}/events/status`,
14
+ };
@@ -0,0 +1,91 @@
1
+ import { AdditionalInfo } from '@workspace/rusty-replay/index';
2
+ import { BaseTimeEntity, PaginatedResponse } from '../types';
3
+ import { QueryKey } from '@tanstack/react-query';
4
+
5
+ export interface EventReportResponse extends BaseTimeEntity {
6
+ id: number;
7
+ message: string;
8
+ stacktrace: string;
9
+ appVersion: string;
10
+ timestamp: string;
11
+ groupHash: string;
12
+ replay: string;
13
+ environment: string;
14
+ browser: string | null;
15
+ os: string | null;
16
+ ipAddress: string | null;
17
+ userAgent: string | null;
18
+ projectId: number;
19
+ issueId: number | null;
20
+ additionalInfo: AdditionalInfo | null;
21
+ priority: EventPriorityType | null;
22
+ assignedTo: number | null;
23
+ status: EventStatusType;
24
+ }
25
+
26
+ export interface EventReportListResponse {
27
+ id: number;
28
+ message: string;
29
+ stacktrace: string;
30
+ appVersion: string;
31
+ timestamp: string;
32
+ groupHash: string;
33
+ issueId: number | null;
34
+ browser: string | null;
35
+ os: string | null;
36
+ hasReplay: boolean;
37
+ priority: EventPriorityType | null;
38
+ assignedTo: number | null;
39
+ status: EventStatusType;
40
+ }
41
+
42
+ export interface EventQuery {
43
+ search: string | null;
44
+ page: number;
45
+ pageSize: number;
46
+ startDate: string | null;
47
+ endDate: string | null;
48
+ }
49
+
50
+ interface EventIds {
51
+ eventIds: number[];
52
+ }
53
+
54
+ export interface EventPriority extends EventIds {
55
+ priority: EventPriorityType;
56
+ }
57
+
58
+ export type EventPriorityType = 'HIGH' | 'MED' | 'LOW';
59
+ export type EventStatusType = 'RESOLVED' | 'UNRESOLVED';
60
+
61
+ export interface EventAssignee extends EventIds {
62
+ assignedTo: number | null;
63
+ }
64
+
65
+ export interface EventStatus extends EventIds {
66
+ status: EventStatusType;
67
+ }
68
+
69
+ export interface EventAssigneeContext {
70
+ previousQueries: [
71
+ QueryKey,
72
+ PaginatedResponse<EventReportListResponse> | undefined,
73
+ ][];
74
+ previousDetailQuery: EventReportResponse | undefined;
75
+ }
76
+
77
+ export interface EventStatusContext {
78
+ previousQueries: [
79
+ QueryKey,
80
+ PaginatedResponse<EventReportListResponse> | undefined,
81
+ ][];
82
+ previousDetailQuery: EventReportResponse | undefined;
83
+ }
84
+
85
+ export interface EventReportListContext {
86
+ previousQueries: [
87
+ QueryKey,
88
+ PaginatedResponse<EventReportListResponse> | undefined,
89
+ ][];
90
+ previousDetailQueries: Record<number, EventReportResponse | undefined>;
91
+ }
@@ -0,0 +1,103 @@
1
+ import {
2
+ useMutation,
3
+ UseMutationOptions,
4
+ useQueryClient,
5
+ } from '@tanstack/react-query';
6
+ import { eventKeys } from './keys';
7
+ import { PaginatedResponse, ResponseError } from '../types';
8
+ import {
9
+ EventAssignee,
10
+ EventAssigneeContext,
11
+ EventReportListResponse,
12
+ EventReportResponse,
13
+ } from './types';
14
+ import axiosInstance from '../axios';
15
+ import { toast } from '@workspace/ui/components/sonner';
16
+
17
+ export function useMutationEventAssignee({
18
+ projectId,
19
+ eventId,
20
+ options,
21
+ }: {
22
+ projectId: number;
23
+ eventId: number;
24
+ options?: UseMutationOptions<
25
+ EventReportListResponse[],
26
+ ResponseError,
27
+ EventAssignee,
28
+ EventAssigneeContext
29
+ >;
30
+ }) {
31
+ const queryClient = useQueryClient();
32
+ const queryKey = eventKeys.list(projectId);
33
+ const detailQueryKey = `/api/projects/${projectId}/events/${eventId}`;
34
+ const mutationKey = eventKeys.assignee(projectId);
35
+ const mutationFn = async (data: EventAssignee) =>
36
+ await axiosInstance.put(mutationKey, data).then((res) => res.data);
37
+
38
+ return useMutation({
39
+ mutationKey: [mutationKey],
40
+ mutationFn,
41
+ onMutate: async (newAssignee) => {
42
+ await queryClient.cancelQueries({
43
+ queryKey: [queryKey],
44
+ });
45
+ await queryClient.cancelQueries({
46
+ queryKey: [detailQueryKey],
47
+ });
48
+
49
+ const previousQueries = queryClient.getQueriesData<
50
+ PaginatedResponse<EventReportListResponse>
51
+ >({
52
+ queryKey: [queryKey],
53
+ });
54
+
55
+ const previousDetailQuery = queryClient.getQueryData<EventReportResponse>(
56
+ [detailQueryKey]
57
+ );
58
+
59
+ queryClient.setQueriesData<PaginatedResponse<EventReportListResponse>>(
60
+ {
61
+ queryKey: [queryKey],
62
+ },
63
+ (old) => {
64
+ if (!old) return old;
65
+
66
+ return {
67
+ ...old,
68
+ content: old.content.map((event) =>
69
+ event.id === eventId
70
+ ? { ...event, assignedTo: newAssignee.assignedTo }
71
+ : event
72
+ ),
73
+ };
74
+ }
75
+ );
76
+
77
+ queryClient.setQueryData<EventReportResponse>([detailQueryKey], (old) => {
78
+ if (!old) return old;
79
+ return {
80
+ ...old,
81
+ assignedTo: newAssignee.assignedTo,
82
+ };
83
+ });
84
+
85
+ return { previousQueries, previousDetailQuery };
86
+ },
87
+ onError: (err, newAssignee, context) => {
88
+ if (context?.previousQueries) {
89
+ context.previousQueries.forEach(([queryKey, data]) => {
90
+ queryClient.setQueryData(queryKey, data);
91
+ });
92
+ }
93
+
94
+ if (context?.previousDetailQuery) {
95
+ queryClient.setQueryData([detailQueryKey], context.previousDetailQuery);
96
+ }
97
+
98
+ console.error(err);
99
+ toast.error('Failed to update assignee');
100
+ },
101
+ ...options,
102
+ });
103
+ }
@@ -0,0 +1,97 @@
1
+ import {
2
+ useMutation,
3
+ UseMutationOptions,
4
+ useQueryClient,
5
+ QueryKey,
6
+ } from '@tanstack/react-query';
7
+ import { ResponseError } from '../types';
8
+ import {
9
+ EventPriority,
10
+ EventReportListContext,
11
+ EventReportListResponse,
12
+ EventReportResponse,
13
+ } from './types';
14
+ import axiosInstance from '../axios';
15
+ import { eventKeys } from './keys';
16
+ import { PaginatedResponse } from '../types';
17
+
18
+ export function useMutationEventPriority({
19
+ projectId,
20
+ options,
21
+ }: {
22
+ projectId: number;
23
+ options?: UseMutationOptions<
24
+ EventReportListResponse[],
25
+ ResponseError,
26
+ EventPriority,
27
+ EventReportListContext
28
+ >;
29
+ }) {
30
+ const queryClient = useQueryClient();
31
+ const listQueryKey = eventKeys.list(projectId);
32
+ const mutationFn = (data: EventPriority) =>
33
+ axiosInstance
34
+ .put(eventKeys.priority(projectId), data)
35
+ .then((res) => res.data);
36
+
37
+ return useMutation({
38
+ mutationKey: [eventKeys.priority(projectId)],
39
+ mutationFn,
40
+ onMutate: async (newData) => {
41
+ const { eventIds, priority } = newData;
42
+
43
+ await queryClient.cancelQueries({ queryKey: [listQueryKey] });
44
+
45
+ const previousQueries = queryClient.getQueriesData<
46
+ PaginatedResponse<EventReportListResponse>
47
+ >({
48
+ queryKey: [listQueryKey],
49
+ });
50
+
51
+ const previousDetailQueries: Record<
52
+ number,
53
+ EventReportResponse | undefined
54
+ > = {};
55
+ eventIds.forEach((id) => {
56
+ const key = [`/api/projects/${projectId}/events/${id}`];
57
+ previousDetailQueries[id] =
58
+ queryClient.getQueryData<EventReportResponse>(key);
59
+ });
60
+
61
+ queryClient.setQueriesData<PaginatedResponse<EventReportListResponse>>(
62
+ { queryKey: [listQueryKey] },
63
+ (old) => {
64
+ if (!old) return old;
65
+ return {
66
+ ...old,
67
+ content: old.content.map((evt) =>
68
+ eventIds.includes(evt.id) ? { ...evt, priority } : evt
69
+ ),
70
+ };
71
+ }
72
+ );
73
+
74
+ eventIds.forEach((id) => {
75
+ const detailKey = [eventKeys.detail(projectId, id)];
76
+ queryClient.setQueryData<EventReportResponse>(detailKey, (old) => {
77
+ if (!old) return old;
78
+ return { ...old, priority };
79
+ });
80
+ });
81
+
82
+ return { previousQueries, previousDetailQueries };
83
+ },
84
+ onError: (err, newData, context) => {
85
+ context?.previousQueries.forEach(([key, data]) => {
86
+ queryClient.setQueryData(key, data);
87
+ });
88
+ if (context?.previousDetailQueries) {
89
+ Object.entries(context.previousDetailQueries).forEach(([id, data]) => {
90
+ const detailKey = [`/api/projects/${projectId}/events/${id}`];
91
+ queryClient.setQueryData(detailKey, data);
92
+ });
93
+ }
94
+ },
95
+ ...options,
96
+ });
97
+ }