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,32 @@
1
+ import js from "@eslint/js"
2
+ import eslintConfigPrettier from "eslint-config-prettier"
3
+ import onlyWarn from "eslint-plugin-only-warn"
4
+ import turboPlugin from "eslint-plugin-turbo"
5
+ import tseslint from "typescript-eslint"
6
+
7
+ /**
8
+ * A shared ESLint configuration for the repository.
9
+ *
10
+ * @type {import("eslint").Linter.Config}
11
+ * */
12
+ export const config = [
13
+ js.configs.recommended,
14
+ eslintConfigPrettier,
15
+ ...tseslint.configs.recommended,
16
+ {
17
+ plugins: {
18
+ turbo: turboPlugin,
19
+ },
20
+ rules: {
21
+ "turbo/no-undeclared-env-vars": "warn",
22
+ },
23
+ },
24
+ {
25
+ plugins: {
26
+ onlyWarn,
27
+ },
28
+ },
29
+ {
30
+ ignores: ["dist/**"],
31
+ },
32
+ ]
@@ -0,0 +1,51 @@
1
+ import js from "@eslint/js"
2
+ import pluginNext from "@next/eslint-plugin-next"
3
+ import eslintConfigPrettier from "eslint-config-prettier"
4
+ import pluginReact from "eslint-plugin-react"
5
+ import pluginReactHooks from "eslint-plugin-react-hooks"
6
+ import globals from "globals"
7
+ import tseslint from "typescript-eslint"
8
+
9
+ import { config as baseConfig } from "./base.js"
10
+
11
+ /**
12
+ * A custom ESLint configuration for libraries that use Next.js.
13
+ *
14
+ * @type {import("eslint").Linter.Config}
15
+ * */
16
+ export const nextJsConfig = [
17
+ ...baseConfig,
18
+ js.configs.recommended,
19
+ eslintConfigPrettier,
20
+ ...tseslint.configs.recommended,
21
+ {
22
+ ...pluginReact.configs.flat.recommended,
23
+ languageOptions: {
24
+ ...pluginReact.configs.flat.recommended.languageOptions,
25
+ globals: {
26
+ ...globals.serviceworker,
27
+ },
28
+ },
29
+ },
30
+ {
31
+ plugins: {
32
+ "@next/next": pluginNext,
33
+ },
34
+ rules: {
35
+ ...pluginNext.configs.recommended.rules,
36
+ ...pluginNext.configs["core-web-vitals"].rules,
37
+ },
38
+ },
39
+ {
40
+ plugins: {
41
+ "react-hooks": pluginReactHooks,
42
+ },
43
+ settings: { react: { version: "detect" } },
44
+ rules: {
45
+ ...pluginReactHooks.configs.recommended.rules,
46
+ // React scope no longer necessary with new JSX transform.
47
+ "react/react-in-jsx-scope": "off",
48
+ "react/prop-types": "off",
49
+ },
50
+ },
51
+ ]
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@workspace/eslint-config",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "private": true,
6
+ "exports": {
7
+ "./base": "./base.js",
8
+ "./next-js": "./next.js",
9
+ "./react-internal": "./react-internal.js"
10
+ },
11
+ "devDependencies": {
12
+ "@next/eslint-plugin-next": "^15.1.7",
13
+ "@typescript-eslint/eslint-plugin": "^8.24.1",
14
+ "@typescript-eslint/parser": "^8.24.1",
15
+ "eslint": "^9.20.1",
16
+ "eslint-config-prettier": "^9.1.0",
17
+ "eslint-plugin-only-warn": "^1.1.0",
18
+ "eslint-plugin-react": "^7.37.4",
19
+ "eslint-plugin-react-hooks": "^5.1.0",
20
+ "eslint-plugin-turbo": "^2.4.2",
21
+ "globals": "^15.15.0",
22
+ "typescript": "^5.7.3",
23
+ "typescript-eslint": "^8.24.1"
24
+ }
25
+ }
@@ -0,0 +1,41 @@
1
+ import js from "@eslint/js"
2
+ import eslintConfigPrettier from "eslint-config-prettier"
3
+ import pluginReact from "eslint-plugin-react"
4
+ import pluginReactHooks from "eslint-plugin-react-hooks"
5
+ import globals from "globals"
6
+ import tseslint from "typescript-eslint"
7
+
8
+ import { config as baseConfig } from "./base.js"
9
+
10
+ /**
11
+ * A custom ESLint configuration for libraries that use React.
12
+ *
13
+ * @type {import("eslint").Linter.Config} */
14
+ export const config = [
15
+ ...baseConfig,
16
+ js.configs.recommended,
17
+ eslintConfigPrettier,
18
+ ...tseslint.configs.recommended,
19
+ pluginReact.configs.flat.recommended,
20
+ {
21
+ languageOptions: {
22
+ ...pluginReact.configs.flat.recommended.languageOptions,
23
+ globals: {
24
+ ...globals.serviceworker,
25
+ ...globals.browser,
26
+ },
27
+ },
28
+ },
29
+ {
30
+ plugins: {
31
+ "react-hooks": pluginReactHooks,
32
+ },
33
+ settings: { react: { version: "detect" } },
34
+ rules: {
35
+ ...pluginReactHooks.configs.recommended.rules,
36
+ // React scope no longer necessary with new JSX transform.
37
+ "react/react-in-jsx-scope": "off",
38
+ "react/prop-types": "off",
39
+ },
40
+ },
41
+ ]
@@ -0,0 +1,165 @@
1
+ # 🦀 rusty-replay
2
+
3
+ 웹 클라이언트에서 발생한 오류를 수집하고, 직전 사용자 활동을 리플레이 형태로 함께 전송하는 경량 오류 추적 도구입니다.
4
+
5
+ `rrweb` 기반의 사용자 행동 리플레이 기능과 `axios` 에러 자동 전송까지 지원합니다.
6
+
7
+ ## 📦 설치
8
+
9
+ ```bash
10
+ npm install rusty-replay
11
+ ```
12
+
13
+ ---
14
+
15
+ ## ⚙️ 초기화
16
+
17
+ Next.js에서 `rusty-replay`는 일반적으로 `app/providers.tsx` 또는 `layout.tsx`에서 초기화합니다.
18
+
19
+ ```ts
20
+ import { init } from 'rusty-replay';
21
+
22
+ init({
23
+ endpoint: 'https://your-api.com/batch-events',
24
+ apiKey: 'YOUR_PUBLIC_API_KEY',
25
+ flushIntervalMs: 10000, // 버퍼가 찰 때까지 최대 대기 시간 (ms)
26
+ maxBufferSize: 2000000, // 전송 전 최대 버퍼 사이즈 (bytes)
27
+ beforeErrorSec: 10, // 에러 발생 전 몇 초간의 이벤트를 리플레이로 남길지
28
+ });
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 🧠 글로벌 에러 자동 캡처
34
+
35
+ ```ts
36
+ import { setupGlobalErrorHandler } from 'rusty-replay';
37
+
38
+ setupGlobalErrorHandler();
39
+ ```
40
+
41
+ - `window.onerror`
42
+ - `window.onunhandledrejection`
43
+ 을 자동 감지하여 오류를 서버로 전송합니다.
44
+
45
+ ---
46
+
47
+ ## 🔧 Axios 에러 자동 전송
48
+
49
+ ```ts
50
+ import axios from 'axios';
51
+ import { captureException, AdditionalInfo } from 'rusty-replay';
52
+
53
+ axios.interceptors.response.use(
54
+ (res) => res,
55
+ (error) => {
56
+ if (axios.isAxiosError(error)) {
57
+ const additionalInfo: Partial<AdditionalInfo> = {
58
+ pageUrl: window.location.href,
59
+ request: {
60
+ url: error.config?.url ?? '',
61
+ method: error.config?.method ?? '',
62
+ headers: error.config?.headers ?? {},
63
+ },
64
+ response: {
65
+ status: error.response?.status ?? 0,
66
+ statusText: error.response?.statusText ?? '',
67
+ data: {
68
+ message: error.response?.data?.message ?? '',
69
+ errorCode: error.response?.data?.errorCode ?? '',
70
+ },
71
+ },
72
+ };
73
+
74
+ captureException(
75
+ error instanceof Error ? error : new Error('API 요청 실패'),
76
+ additionalInfo
77
+ );
78
+ }
79
+
80
+ return Promise.reject(error);
81
+ }
82
+ );
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 🖥️ React Error Boundary 통합
88
+
89
+ ```tsx
90
+ import { ErrorBoundary } from 'react-error-boundary';
91
+ import { captureException } from 'rusty-replay';
92
+
93
+ <ErrorBoundaryFallbackComponent={() => <div>오류가 발생했습니다.</div>}
94
+ onError={(error, info) => {
95
+ captureException(error);
96
+ }}
97
+ >
98
+ <App />
99
+ </ErrorBoundary>
100
+
101
+ ```
102
+
103
+ ---
104
+
105
+ ## 🔁 리플레이 재생 (rrweb-player)
106
+
107
+ ```tsx
108
+ import { decompressFromBase64 } from 'rusty-replay';
109
+ import 'rrweb-player/dist/style.css';
110
+
111
+ const events = decompressFromBase64(error.replay);
112
+
113
+ new Player({
114
+ target: document.getElementById('player')!,
115
+ props: {
116
+ events,
117
+ width: 1000,
118
+ height: 600,
119
+ autoPlay: false,
120
+ showController: true,
121
+ skipInactive: true,
122
+ },
123
+ });
124
+ ```
125
+
126
+ ---
127
+
128
+ ## 📋 서버로 전송되는 데이터 Payload
129
+
130
+ ```ts
131
+ {
132
+ message: 'Uncaught TypeError: ...',
133
+ stacktrace: 'TypeError: ...',
134
+ replay: 'compressedBase64Data',
135
+ environment: 'production',
136
+ browser: 'Chrome 123.0',
137
+ os: 'macOS 14',
138
+ userAgent: '...',
139
+ appVersion: '1.0.0',
140
+ apiKey: 'YOUR_API_KEY',
141
+ additionalInfo: {
142
+ request: {...},
143
+ response: {...},
144
+ pageUrl: 'https://your.site/path'
145
+ },
146
+ userId: 123
147
+ }
148
+
149
+ ```
150
+
151
+ ---
152
+
153
+ ## 📎 API 참고
154
+
155
+ ### `init(options: InitOptions)`
156
+
157
+ 옵션 설명:
158
+
159
+ | 옵션 | 타입 | 설명 |
160
+ | ----------------- | ------ | -------------------------------------- |
161
+ | `endpoint` | string | 에러 수집 서버의 엔드포인트 |
162
+ | `apiKey` | string | 프로젝트 식별용 API Key |
163
+ | `flushIntervalMs` | number | 에러 전송 간격 (기본: 10초) |
164
+ | `maxBufferSize` | number | 최대 전송 버퍼 크기 |
165
+ | `beforeErrorSec` | number | 리플레이 수집 구간 (기본: 10초 전까지) |
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "rusty-replay",
3
+ "version": "1.0.13",
4
+ "description": "Lightweight error tracking and replay system for React apps using rrweb and Rust-powered backend integration.",
5
+ "main": "dist/index.cjs",
6
+ "module": "dist/index.js",
7
+ "type": "module",
8
+ "types": "dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "keywords": [
14
+ "rrweb",
15
+ "replay",
16
+ "error-tracking",
17
+ "frontend",
18
+ "typescript",
19
+ "react",
20
+ "cli",
21
+ "monitoring"
22
+ ],
23
+ "browser": {
24
+ "require-in-the-middle": false,
25
+ "import-in-the-middle": false,
26
+ "@opentelemetry/instrumentation": false
27
+ },
28
+ "author": "Jaeha Lee <wogkdkrm112@gmail.com>",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/rusty-replay/replay"
33
+ },
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "prepublishOnly": "npm run build"
37
+ },
38
+ "peerDependencies": {
39
+ "@rrweb/types": "^2.0.0-alpha.4",
40
+ "react": "^16.8 || ^17 || ^18 || ^19",
41
+ "rrweb": "^2.0.0-alpha.4",
42
+ "rrweb-player": "^1.0.0-alpha.4",
43
+ "import-in-the-middle": "^1.13.1",
44
+ "require-in-the-middle": "^7.5.2"
45
+ },
46
+ "dependencies": {
47
+ "@opentelemetry/context-zone": "^2.0.0",
48
+ "@opentelemetry/core": "^2.0.0",
49
+ "@opentelemetry/exporter-trace-otlp-proto": "^0.200.0",
50
+ "@opentelemetry/instrumentation": "^0.200.0",
51
+ "@opentelemetry/instrumentation-fetch": "^0.200.0",
52
+ "@opentelemetry/instrumentation-xml-http-request": "^0.200.0",
53
+ "@opentelemetry/resources": "^2.0.0",
54
+ "@opentelemetry/sdk-trace-base": "^2.0.0",
55
+ "@opentelemetry/sdk-trace-web": "^2.0.0",
56
+ "@opentelemetry/semantic-conventions": "^1.32.0",
57
+ "@rrweb/packer": "2.0.0-alpha.18",
58
+ "axios": "^1.8.4",
59
+ "fflate": "^0.8.2",
60
+ "rrweb-snapshot": "2.0.0-alpha.4"
61
+ },
62
+ "devDependencies": {
63
+ "@types/node": "^20",
64
+ "tsup": "^8.4.0",
65
+ "typescript": "^5.8.3"
66
+ }
67
+ }
@@ -0,0 +1,27 @@
1
+ export function getBrowserInfo() {
2
+ const ua = navigator.userAgent;
3
+ let browser = 'unknown',
4
+ os = 'unknown';
5
+
6
+ if (ua.includes('Firefox')) browser = 'Firefox';
7
+ else if (ua.includes('SamsungBrowser')) browser = 'Samsung Browser';
8
+ else if (ua.includes('Opera') || ua.includes('OPR')) browser = 'Opera';
9
+ else if (ua.includes('Trident')) browser = 'IE';
10
+ else if (ua.includes('Edge')) browser = 'Edge (Legacy)';
11
+ else if (ua.includes('Edg')) browser = 'Edge';
12
+ else if (ua.includes('Chrome')) browser = 'Chrome';
13
+ else if (ua.includes('Safari')) browser = 'Safari';
14
+
15
+ if (ua.includes('Windows')) os = 'Windows';
16
+ else if (ua.includes('Mac')) os = 'macOS';
17
+ else if (ua.includes('Linux')) os = 'Linux';
18
+ else if (ua.includes('Android')) os = 'Android';
19
+ else if (ua.includes('like Mac')) os = 'iOS';
20
+
21
+ return { browser, os, userAgent: ua };
22
+ }
23
+
24
+ export function getEnvironment(): 'development' | 'staging' | 'production' {
25
+ if (process.env.NODE_ENV === 'development') return 'development';
26
+ return 'production';
27
+ }
@@ -0,0 +1,75 @@
1
+ import axios from 'axios';
2
+ import type { BatcherOptions, BatchedEvent } from './reporter';
3
+
4
+ export class ErrorBatcher {
5
+ private queue: BatchedEvent[] = [];
6
+ private isFlushing = false;
7
+ private flushTimer: number;
8
+ private readonly apiKey: string;
9
+
10
+ constructor(private opts: BatcherOptions) {
11
+ this.apiKey = opts.apiKey;
12
+ const interval = opts.flushIntervalMs ?? 30000;
13
+ this.flushTimer = window.setInterval(() => this.flush(), interval);
14
+ window.addEventListener('beforeunload', () => this.flushOnUnload());
15
+ }
16
+
17
+ public getApiKey(): string {
18
+ return this.apiKey;
19
+ }
20
+
21
+ public capture(evt: Omit<BatchedEvent, 'id' | 'timestamp'>): string {
22
+ const id = this.makeId();
23
+ const timestamp = new Date().toISOString();
24
+ const record: BatchedEvent = { id, timestamp, ...evt };
25
+
26
+ if (this.queue.length >= (this.opts.maxBufferSize ?? 64)) {
27
+ this.queue.shift();
28
+ }
29
+ this.queue.push(record);
30
+ return id;
31
+ }
32
+
33
+ private async flush() {
34
+ if (this.isFlushing || this.queue.length === 0) return;
35
+ this.isFlushing = true;
36
+
37
+ const batch = this.queue.splice(0, this.queue.length);
38
+ try {
39
+ await axios.post(
40
+ this.opts.endpoint,
41
+ { events: batch },
42
+ {
43
+ maxBodyLength: 1000 * 1024 * 1024, // 10MB
44
+ maxContentLength: 1000 * 1024 * 1024, // 10MB
45
+ timeout: 30000,
46
+ headers: {
47
+ 'Content-Type': 'application/json',
48
+ },
49
+ }
50
+ );
51
+ } catch {
52
+ this.queue.unshift(...batch);
53
+ } finally {
54
+ this.isFlushing = false;
55
+ }
56
+ }
57
+
58
+ private flushOnUnload() {
59
+ if (!navigator.sendBeacon || this.queue.length === 0) return;
60
+ const payload = JSON.stringify({ events: this.queue });
61
+ navigator.sendBeacon(this.opts.endpoint, payload);
62
+ }
63
+
64
+ private makeId() {
65
+ return 'xxxx-xxxx-4xxx-yxxx'.replace(/[xy]/g, (c) => {
66
+ const r = (Math.random() * 16) | 0;
67
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
68
+ return v.toString(16);
69
+ });
70
+ }
71
+
72
+ public destroy() {
73
+ clearInterval(this.flushTimer);
74
+ }
75
+ }
@@ -0,0 +1,86 @@
1
+ import { W3CTraceContextPropagator } from '@opentelemetry/core';
2
+ import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
3
+ import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
4
+ import {
5
+ resourceFromAttributes,
6
+ osDetector,
7
+ detectResources,
8
+ } from '@opentelemetry/resources';
9
+ import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
10
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
11
+
12
+ export interface OtelConfig {
13
+ serviceName?: string;
14
+ endpoint?: string;
15
+ isSyntheticRequest?: boolean;
16
+ scheduledDelayMillis?: number;
17
+ customHeaders?: Record<string, string>;
18
+ }
19
+
20
+ export const initOtel = async (config: OtelConfig = {}) => {
21
+ const finalConfig = {
22
+ serviceName: config.serviceName ?? 'replay',
23
+ endpoint: config.endpoint ?? 'http://localhost:8081/traces',
24
+ isSyntheticRequest: config.isSyntheticRequest ?? false,
25
+ scheduledDelayMillis: config.scheduledDelayMillis ?? 500,
26
+ customHeaders: {
27
+ 'Content-Type': 'application/x-protobuf',
28
+ ...config.customHeaders,
29
+ },
30
+ };
31
+
32
+ const { ZoneContextManager } = await import('@opentelemetry/context-zone');
33
+
34
+ let resource = resourceFromAttributes({
35
+ [ATTR_SERVICE_NAME]: finalConfig.serviceName,
36
+ });
37
+ if (finalConfig.isSyntheticRequest) {
38
+ resource = resource.merge(
39
+ resourceFromAttributes({ 'app.synthetic_request': 'true' })
40
+ );
41
+ }
42
+ resource = resource.merge(detectResources({ detectors: [osDetector] }));
43
+
44
+ const spanProcessor = new BatchSpanProcessor(
45
+ new OTLPTraceExporter({
46
+ url: finalConfig.endpoint,
47
+ headers: finalConfig.customHeaders,
48
+ }),
49
+ { scheduledDelayMillis: finalConfig.scheduledDelayMillis }
50
+ );
51
+ const provider = new WebTracerProvider({
52
+ resource,
53
+ spanProcessors: [spanProcessor],
54
+ });
55
+ provider.register({
56
+ contextManager: new ZoneContextManager(),
57
+ propagator: new W3CTraceContextPropagator(),
58
+ });
59
+
60
+ if (typeof window !== 'undefined') {
61
+ const backendOrigin = new URL(finalConfig.endpoint).origin;
62
+
63
+ const [{ FetchInstrumentation }, { XMLHttpRequestInstrumentation }] =
64
+ await Promise.all([
65
+ import('@opentelemetry/instrumentation-fetch'),
66
+ import('@opentelemetry/instrumentation-xml-http-request'),
67
+ ]);
68
+
69
+ const fetchInst = new FetchInstrumentation({
70
+ propagateTraceHeaderCorsUrls: [backendOrigin],
71
+ clearTimingResources: true,
72
+ });
73
+ const xhrInst = new XMLHttpRequestInstrumentation({
74
+ propagateTraceHeaderCorsUrls: [backendOrigin],
75
+ clearTimingResources: true,
76
+ });
77
+
78
+ fetchInst.setTracerProvider(provider);
79
+ xhrInst.setTracerProvider(provider);
80
+ fetchInst.enable();
81
+ xhrInst.enable();
82
+ }
83
+
84
+ console.log('OpenTelemetry initialized successfully');
85
+ return provider;
86
+ };
@@ -0,0 +1,37 @@
1
+ import { captureException } from './reporter';
2
+
3
+ export function setupGlobalErrorHandler() {
4
+ if ((window as any).__errorHandlerSetup) return;
5
+
6
+ const origOnError = window.onerror;
7
+ window.onerror = function thisWindowOnError(
8
+ this: Window & WindowEventHandlers,
9
+ message: string | Event,
10
+ source?: string,
11
+ lineno?: number,
12
+ colno?: number,
13
+ error?: Error
14
+ ): boolean {
15
+ origOnError?.call(this, message, source, lineno, colno, error);
16
+ captureException(
17
+ error ??
18
+ new Error(typeof message === 'string' ? message : 'Unknown error')
19
+ );
20
+ return false;
21
+ };
22
+
23
+ const origOnUnhandledRejection = window.onunhandledrejection;
24
+ window.onunhandledrejection = function thisWindowOnRejection(
25
+ this: Window & WindowEventHandlers,
26
+ event: PromiseRejectionEvent
27
+ ): any {
28
+ origOnUnhandledRejection?.call(this, event);
29
+ const err =
30
+ event.reason instanceof Error
31
+ ? event.reason
32
+ : new Error(JSON.stringify(event.reason));
33
+ captureException(err);
34
+ } as typeof window.onunhandledrejection;
35
+
36
+ (window as any).__errorHandlerSetup = true;
37
+ }
@@ -0,0 +1,8 @@
1
+ export * from './reporter';
2
+ export { setupGlobalErrorHandler } from './handler';
3
+ export { startRecording, getRecordedEvents } from './recorder';
4
+ export { getBrowserInfo, getEnvironment } from './environment';
5
+ export { ErrorBatcher } from './error-batcher';
6
+ export type { BatchedEvent, InitOptions, AdditionalInfo } from './reporter';
7
+ export { decompressFromBase64 } from './utils';
8
+ export { initOtel } from './front-end-tracer';
@@ -0,0 +1,71 @@
1
+ import type { eventWithTime, listenerHandler } from '@rrweb/types';
2
+ import { record } from 'rrweb';
3
+
4
+ let events: eventWithTime[] = [];
5
+ const MAX_EVENTS = 1000;
6
+ let stopFn: listenerHandler | undefined = undefined;
7
+
8
+ export function startRecording() {
9
+ events = [];
10
+ stopFn?.();
11
+ stopFn = record({
12
+ emit(event) {
13
+ if (event.type === 2) console.log('[rrweb] FullSnapshot 기록됨:', event);
14
+
15
+ events.push(event);
16
+ if (events.length > MAX_EVENTS) {
17
+ events = events.slice(-MAX_EVENTS);
18
+ }
19
+ },
20
+ // checkoutEveryNms: 1000, // 1초마다 체크아웃
21
+ checkoutEveryNms: 15000, // 15초마다 한 번
22
+ checkoutEveryNth: 100, // 100개 이벤트마다 한 번
23
+ maskAllInputs: true,
24
+ sampling: {
25
+ mouseInteraction: {
26
+ MouseUp: false,
27
+ MouseDown: false,
28
+ Click: false,
29
+ ContextMenu: false,
30
+ DblClick: false,
31
+ Focus: false,
32
+ Blur: false,
33
+ TouchStart: false,
34
+ TouchEnd: false,
35
+ },
36
+ },
37
+ });
38
+ }
39
+
40
+ export function getRecordedEvents(
41
+ beforeErrorSec = 10,
42
+ errorTime = Date.now(),
43
+ source = events
44
+ ): eventWithTime[] {
45
+ const sliced = source.filter(
46
+ (e) => errorTime - e.timestamp < beforeErrorSec * 1000
47
+ );
48
+
49
+ const snapshotCandidates = source.filter((e) => e.type === 2);
50
+ const lastSnapshot = [...snapshotCandidates]
51
+ .reverse()
52
+ .find((e) => e.timestamp <= errorTime);
53
+
54
+ if (lastSnapshot && !sliced.includes(lastSnapshot)) {
55
+ return [lastSnapshot, ...sliced];
56
+ }
57
+
58
+ if (!sliced.some((e) => e.type === 2)) {
59
+ console.warn('⚠️ Snapshot 없이 잘린 replay입니다. 복원 불가능할 수 있음.');
60
+ }
61
+
62
+ return sliced;
63
+ }
64
+
65
+ export function clearEvents() {
66
+ events = [];
67
+ }
68
+
69
+ export function getCurrentEvents(): eventWithTime[] {
70
+ return events.slice();
71
+ }