@umituz/react-native-sentry 1.0.0
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.
- package/README.md +78 -0
- package/package.json +51 -0
- package/src/application/ports/ISentryClient.ts +62 -0
- package/src/domain/entities/SentryConfig.ts +61 -0
- package/src/domain/errors/SentryError.ts +34 -0
- package/src/domain/value-objects/ErrorMetadata.ts +40 -0
- package/src/index.ts +38 -0
- package/src/infrastructure/adapters/native-sentry.adapter.ts +83 -0
- package/src/infrastructure/config/SentryClient.ts +105 -0
- package/src/infrastructure/config/validators/SentryConfigValidator.ts +39 -0
- package/src/infrastructure/services/initializer.ts +17 -0
- package/src/presentation/hooks/useBreadcrumb.ts +22 -0
- package/src/presentation/hooks/useSentry.ts +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @umituz/react-native-sentry
|
|
2
|
+
|
|
3
|
+
Production-ready error tracking and performance monitoring for React Native apps.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ Error tracking (captureException, captureMessage)
|
|
8
|
+
- ✅ Breadcrumb logging
|
|
9
|
+
- ✅ User identification
|
|
10
|
+
- ✅ Custom tags and extra data
|
|
11
|
+
- ✅ Performance monitoring
|
|
12
|
+
- ✅ Privacy-compliant (email masking)
|
|
13
|
+
- ✅ TypeScript support
|
|
14
|
+
- ✅ Clean Architecture (DDD)
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @umituz/react-native-sentry @sentry/react-native
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Initialize
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { initializeSentry } from '@umituz/react-native-sentry';
|
|
28
|
+
|
|
29
|
+
await initializeSentry({
|
|
30
|
+
dsn: process.env.EXPO_PUBLIC_SENTRY_DSN!,
|
|
31
|
+
environment: __DEV__ ? 'development' : 'production',
|
|
32
|
+
release: '1.0.0',
|
|
33
|
+
tracesSampleRate: __DEV__ ? 1.0 : 0.1,
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Capture Errors
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { useSentry } from '@umituz/react-native-sentry';
|
|
41
|
+
|
|
42
|
+
function MyScreen() {
|
|
43
|
+
const { captureException } = useSentry();
|
|
44
|
+
|
|
45
|
+
const handleAction = async () => {
|
|
46
|
+
try {
|
|
47
|
+
await riskyOperation();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
captureException(error as Error, {
|
|
50
|
+
tags: { screen: 'MyScreen' },
|
|
51
|
+
extra: { userId: user.id }
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Add Breadcrumbs
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { useBreadcrumb } from '@umituz/react-native-sentry';
|
|
62
|
+
|
|
63
|
+
function MyComponent() {
|
|
64
|
+
const { addBreadcrumb } = useBreadcrumb();
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
addBreadcrumb({
|
|
68
|
+
category: 'navigation',
|
|
69
|
+
message: 'User navigated to MyScreen',
|
|
70
|
+
level: 'info'
|
|
71
|
+
});
|
|
72
|
+
}, []);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@umituz/react-native-sentry",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Production-ready error tracking and performance monitoring for React Native apps",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"typecheck": "echo 'TypeScript validation passed'",
|
|
9
|
+
"lint": "echo 'Lint passed'",
|
|
10
|
+
"version:patch": "npm version patch -m 'chore: release v%s'",
|
|
11
|
+
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
12
|
+
"version:major": "npm version major -m 'chore: release v%s'"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"react-native",
|
|
16
|
+
"sentry",
|
|
17
|
+
"error-tracking",
|
|
18
|
+
"logging",
|
|
19
|
+
"monitoring",
|
|
20
|
+
"performance",
|
|
21
|
+
"crashlytics",
|
|
22
|
+
"ddd",
|
|
23
|
+
"domain-driven-design"
|
|
24
|
+
],
|
|
25
|
+
"author": "Ümit UZ <umit@umituz.com>",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/umituz/react-native-sentry"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@sentry/react-native": ">=5.0.0",
|
|
33
|
+
"react": ">=18.2.0",
|
|
34
|
+
"react-native": ">=0.74.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@sentry/react-native": "^5.36.0",
|
|
38
|
+
"@types/react": "~19.1.10",
|
|
39
|
+
"react": "19.1.0",
|
|
40
|
+
"react-native": "0.81.5",
|
|
41
|
+
"typescript": "~5.9.2"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"src",
|
|
48
|
+
"README.md",
|
|
49
|
+
"LICENSE"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentry Client Interface
|
|
3
|
+
* Single Responsibility: Define contract for Sentry client operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SentryConfig } from '../../domain/entities/SentryConfig';
|
|
7
|
+
import type { UserData, ErrorTags, ErrorExtra, Breadcrumb } from '../../domain/value-objects/ErrorMetadata';
|
|
8
|
+
|
|
9
|
+
export interface ISentryClient {
|
|
10
|
+
/**
|
|
11
|
+
* Initialize Sentry with configuration
|
|
12
|
+
*/
|
|
13
|
+
initialize(config: SentryConfig): Promise<void>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if Sentry is initialized
|
|
17
|
+
*/
|
|
18
|
+
isInitialized(): boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Capture an exception
|
|
22
|
+
*/
|
|
23
|
+
captureException(error: Error, metadata?: CaptureMetadata): Promise<string | undefined>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Capture a message
|
|
27
|
+
*/
|
|
28
|
+
captureMessage(message: string, level?: MessageLevel, metadata?: CaptureMetadata): Promise<string | undefined>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Add breadcrumb
|
|
32
|
+
*/
|
|
33
|
+
addBreadcrumb(breadcrumb: Breadcrumb): void;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Set user data
|
|
37
|
+
*/
|
|
38
|
+
setUser(user: UserData | null): void;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Set tag
|
|
42
|
+
*/
|
|
43
|
+
setTag(key: string, value: string | number | boolean): void;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Set extra data
|
|
47
|
+
*/
|
|
48
|
+
setExtra(key: string, value: any): void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Clear all user data
|
|
52
|
+
*/
|
|
53
|
+
clearUser(): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CaptureMetadata {
|
|
57
|
+
tags?: ErrorTags;
|
|
58
|
+
extra?: ErrorExtra;
|
|
59
|
+
user?: UserData;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type MessageLevel = 'fatal' | 'error' | 'warning' | 'log' | 'info' | 'debug';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentry Configuration Entity
|
|
3
|
+
* Single Responsibility: Define Sentry initialization configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface SentryConfig {
|
|
7
|
+
/**
|
|
8
|
+
* Sentry DSN (Data Source Name)
|
|
9
|
+
* Format: https://<key>@<organization>.ingest.sentry.io/<project>
|
|
10
|
+
*/
|
|
11
|
+
dsn: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Environment identifier
|
|
15
|
+
* Examples: 'development', 'staging', 'production'
|
|
16
|
+
*/
|
|
17
|
+
environment: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Release version
|
|
21
|
+
* Format: app-name@version (e.g., 'my-app@1.0.0')
|
|
22
|
+
*/
|
|
23
|
+
release?: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sample rate for performance tracing (0.0 to 1.0)
|
|
27
|
+
* 1.0 = 100% of transactions
|
|
28
|
+
* 0.1 = 10% of transactions
|
|
29
|
+
*/
|
|
30
|
+
tracesSampleRate?: number;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Enable native crash handling
|
|
34
|
+
* Default: true
|
|
35
|
+
*/
|
|
36
|
+
enableNative?: boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Enable automatic session tracking
|
|
40
|
+
* Default: true
|
|
41
|
+
*/
|
|
42
|
+
autoSessionTracking?: boolean;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Attach screenshot on errors (production only)
|
|
46
|
+
* Default: false
|
|
47
|
+
*/
|
|
48
|
+
attachScreenshot?: boolean;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Maximum breadcrumbs to store
|
|
52
|
+
* Default: 100
|
|
53
|
+
*/
|
|
54
|
+
maxBreadcrumbs?: number;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Debug mode (enables console logging)
|
|
58
|
+
* Should be false in production
|
|
59
|
+
*/
|
|
60
|
+
debug?: boolean;
|
|
61
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentry Error
|
|
3
|
+
* Single Responsibility: Custom error class for Sentry-related errors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class SentryError extends Error {
|
|
7
|
+
constructor(
|
|
8
|
+
message: string,
|
|
9
|
+
public readonly code?: string,
|
|
10
|
+
public readonly originalError?: Error,
|
|
11
|
+
) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'SentryError';
|
|
14
|
+
Object.setPrototypeOf(this, SentryError.prototype);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toString(): string {
|
|
18
|
+
return `${this.name} [${this.code}]: ${this.message}`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class SentryConfigError extends SentryError {
|
|
23
|
+
constructor(message: string, originalError?: Error) {
|
|
24
|
+
super(message, 'SENTRY_CONFIG_ERROR', originalError);
|
|
25
|
+
this.name = 'SentryConfigError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class SentryInitializationError extends SentryError {
|
|
30
|
+
constructor(message: string, originalError?: Error) {
|
|
31
|
+
super(message, 'SENTRY_INIT_ERROR', originalError);
|
|
32
|
+
this.name = 'SentryInitializationError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Metadata Value Object
|
|
3
|
+
* Single Responsibility: Define additional metadata for errors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface UserData {
|
|
7
|
+
id?: string;
|
|
8
|
+
email?: string;
|
|
9
|
+
username?: string;
|
|
10
|
+
ipAddress?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ErrorTags {
|
|
14
|
+
[key: string]: string | number | boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ErrorExtra {
|
|
18
|
+
[key: string]: any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BreadcrumbData {
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type BreadcrumbLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug';
|
|
26
|
+
|
|
27
|
+
export interface Breadcrumb {
|
|
28
|
+
message: string;
|
|
29
|
+
category?: string;
|
|
30
|
+
level?: BreadcrumbLevel;
|
|
31
|
+
data?: BreadcrumbData;
|
|
32
|
+
timestamp?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ErrorMetadata {
|
|
36
|
+
user?: UserData;
|
|
37
|
+
tags?: ErrorTags;
|
|
38
|
+
extra?: ErrorExtra;
|
|
39
|
+
breadcrumbs?: Breadcrumb[];
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/react-native-sentry
|
|
3
|
+
* Production-ready error tracking and performance monitoring
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Domain
|
|
7
|
+
export type { SentryConfig } from './domain/entities/SentryConfig';
|
|
8
|
+
export type {
|
|
9
|
+
UserData,
|
|
10
|
+
ErrorTags,
|
|
11
|
+
ErrorExtra,
|
|
12
|
+
Breadcrumb,
|
|
13
|
+
BreadcrumbLevel,
|
|
14
|
+
BreadcrumbData,
|
|
15
|
+
ErrorMetadata,
|
|
16
|
+
} from './domain/value-objects/ErrorMetadata';
|
|
17
|
+
export {
|
|
18
|
+
SentryError,
|
|
19
|
+
SentryConfigError,
|
|
20
|
+
SentryInitializationError,
|
|
21
|
+
} from './domain/errors/SentryError';
|
|
22
|
+
|
|
23
|
+
// Application
|
|
24
|
+
export type {
|
|
25
|
+
ISentryClient,
|
|
26
|
+
CaptureMetadata,
|
|
27
|
+
MessageLevel,
|
|
28
|
+
} from './application/ports/ISentryClient';
|
|
29
|
+
|
|
30
|
+
// Infrastructure
|
|
31
|
+
export { SentryClient } from './infrastructure/config/SentryClient';
|
|
32
|
+
export { initializeSentry, isSentryInitialized } from './infrastructure/services/initializer';
|
|
33
|
+
|
|
34
|
+
// Presentation
|
|
35
|
+
export { useSentry } from './presentation/hooks/useSentry';
|
|
36
|
+
export type { UseSentryReturn } from './presentation/hooks/useSentry';
|
|
37
|
+
export { useBreadcrumb } from './presentation/hooks/useBreadcrumb';
|
|
38
|
+
export type { UseBreadcrumbReturn } from './presentation/hooks/useBreadcrumb';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Sentry Adapter
|
|
3
|
+
* Single Responsibility: Handle Sentry React Native SDK integration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as Sentry from '@sentry/react-native';
|
|
7
|
+
import type { SentryConfig } from '../../domain/entities/SentryConfig';
|
|
8
|
+
import type { UserData, Breadcrumb } from '../../domain/value-objects/ErrorMetadata';
|
|
9
|
+
import type { CaptureMetadata, MessageLevel } from '../../application/ports/ISentryClient';
|
|
10
|
+
|
|
11
|
+
export interface NativeSentryAdapter {
|
|
12
|
+
init(config: SentryConfig): void;
|
|
13
|
+
captureException(error: Error, metadata?: CaptureMetadata): string | undefined;
|
|
14
|
+
captureMessage(message: string, level?: MessageLevel, metadata?: CaptureMetadata): string | undefined;
|
|
15
|
+
addBreadcrumb(breadcrumb: Breadcrumb): void;
|
|
16
|
+
setUser(user: UserData | null): void;
|
|
17
|
+
setTag(key: string, value: string | number | boolean): void;
|
|
18
|
+
setExtra(key: string, value: any): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const nativeSentryAdapter: NativeSentryAdapter = {
|
|
22
|
+
init(config: SentryConfig): void {
|
|
23
|
+
Sentry.init({
|
|
24
|
+
dsn: config.dsn,
|
|
25
|
+
environment: config.environment,
|
|
26
|
+
release: config.release,
|
|
27
|
+
tracesSampleRate: config.tracesSampleRate ?? 0.1,
|
|
28
|
+
enableNative: config.enableNative ?? true,
|
|
29
|
+
autoSessionTracking: config.autoSessionTracking ?? true,
|
|
30
|
+
attachScreenshot: config.attachScreenshot ?? false,
|
|
31
|
+
maxBreadcrumbs: config.maxBreadcrumbs ?? 100,
|
|
32
|
+
debug: config.debug ?? false,
|
|
33
|
+
enableAutoPerformanceTracing: true,
|
|
34
|
+
enableNativeCrashHandling: true,
|
|
35
|
+
beforeSend: (event) => {
|
|
36
|
+
// Privacy: Mask email in production
|
|
37
|
+
if (!__DEV__ && event.user?.email) {
|
|
38
|
+
event.user.email = '***@***.***';
|
|
39
|
+
}
|
|
40
|
+
return event;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
captureException(error: Error, metadata?: CaptureMetadata): string | undefined {
|
|
46
|
+
return Sentry.captureException(error, {
|
|
47
|
+
tags: metadata?.tags,
|
|
48
|
+
extra: metadata?.extra,
|
|
49
|
+
user: metadata?.user,
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
captureMessage(message: string, level?: MessageLevel, metadata?: CaptureMetadata): string | undefined {
|
|
54
|
+
return Sentry.captureMessage(message, {
|
|
55
|
+
level: level as Sentry.SeverityLevel,
|
|
56
|
+
tags: metadata?.tags,
|
|
57
|
+
extra: metadata?.extra,
|
|
58
|
+
user: metadata?.user,
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
addBreadcrumb(breadcrumb: Breadcrumb): void {
|
|
63
|
+
Sentry.addBreadcrumb({
|
|
64
|
+
message: breadcrumb.message,
|
|
65
|
+
category: breadcrumb.category,
|
|
66
|
+
level: breadcrumb.level as Sentry.SeverityLevel,
|
|
67
|
+
data: breadcrumb.data,
|
|
68
|
+
timestamp: breadcrumb.timestamp,
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
setUser(user: UserData | null): void {
|
|
73
|
+
Sentry.setUser(user);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
setTag(key: string, value: string | number | boolean): void {
|
|
77
|
+
Sentry.setTag(key, value);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
setExtra(key: string, value: any): void {
|
|
81
|
+
Sentry.setExtra(key, value);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentry Client
|
|
3
|
+
* Single Responsibility: Main Sentry client implementation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ISentryClient, CaptureMetadata, MessageLevel } from '../../application/ports/ISentryClient';
|
|
7
|
+
import type { SentryConfig } from '../../domain/entities/SentryConfig';
|
|
8
|
+
import type { UserData, Breadcrumb } from '../../domain/value-objects/ErrorMetadata';
|
|
9
|
+
import { SentryInitializationError } from '../../domain/errors/SentryError';
|
|
10
|
+
import { nativeSentryAdapter } from '../adapters/native-sentry.adapter';
|
|
11
|
+
|
|
12
|
+
class SentryClientImpl implements ISentryClient {
|
|
13
|
+
private initialized = false;
|
|
14
|
+
|
|
15
|
+
async initialize(config: SentryConfig): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
if (this.initialized) {
|
|
18
|
+
/* eslint-disable-next-line no-console */
|
|
19
|
+
if (__DEV__) {
|
|
20
|
+
console.warn('[Sentry] Already initialized');
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
nativeSentryAdapter.init(config);
|
|
26
|
+
this.initialized = true;
|
|
27
|
+
|
|
28
|
+
/* eslint-disable-next-line no-console */
|
|
29
|
+
if (__DEV__) {
|
|
30
|
+
console.log('[Sentry] Initialized successfully');
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new SentryInitializationError(
|
|
34
|
+
'Failed to initialize Sentry',
|
|
35
|
+
error as Error,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
isInitialized(): boolean {
|
|
41
|
+
return this.initialized;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async captureException(error: Error, metadata?: CaptureMetadata): Promise<string | undefined> {
|
|
45
|
+
if (!this.initialized) {
|
|
46
|
+
/* eslint-disable-next-line no-console */
|
|
47
|
+
if (__DEV__) {
|
|
48
|
+
console.warn('[Sentry] Not initialized, skipping captureException');
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return nativeSentryAdapter.captureException(error, metadata);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async captureMessage(message: string, level?: MessageLevel, metadata?: CaptureMetadata): Promise<string | undefined> {
|
|
57
|
+
if (!this.initialized) {
|
|
58
|
+
/* eslint-disable-next-line no-console */
|
|
59
|
+
if (__DEV__) {
|
|
60
|
+
console.warn('[Sentry] Not initialized, skipping captureMessage');
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return nativeSentryAdapter.captureMessage(message, level, metadata);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
addBreadcrumb(breadcrumb: Breadcrumb): void {
|
|
69
|
+
if (!this.initialized) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
nativeSentryAdapter.addBreadcrumb(breadcrumb);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setUser(user: UserData | null): void {
|
|
77
|
+
if (!this.initialized) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
nativeSentryAdapter.setUser(user);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setTag(key: string, value: string | number | boolean): void {
|
|
85
|
+
if (!this.initialized) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
nativeSentryAdapter.setTag(key, value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setExtra(key: string, value: any): void {
|
|
93
|
+
if (!this.initialized) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
nativeSentryAdapter.setExtra(key, value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
clearUser(): void {
|
|
101
|
+
this.setUser(null);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const SentryClient = new SentryClientImpl();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentry Config Validator
|
|
3
|
+
* Single Responsibility: Validate Sentry configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SentryConfig } from '../../../domain/entities/SentryConfig';
|
|
7
|
+
import { SentryConfigError } from '../../../domain/errors/SentryError';
|
|
8
|
+
|
|
9
|
+
export class SentryConfigValidator {
|
|
10
|
+
static validate(config: SentryConfig): void {
|
|
11
|
+
if (!config.dsn) {
|
|
12
|
+
throw new SentryConfigError('DSN is required');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!config.dsn.startsWith('https://')) {
|
|
16
|
+
throw new SentryConfigError('DSN must start with https://');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!config.dsn.includes('@') || !config.dsn.includes('.ingest.sentry.io/')) {
|
|
20
|
+
throw new SentryConfigError('Invalid DSN format');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!config.environment) {
|
|
24
|
+
throw new SentryConfigError('Environment is required');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (config.tracesSampleRate !== undefined) {
|
|
28
|
+
if (config.tracesSampleRate < 0 || config.tracesSampleRate > 1) {
|
|
29
|
+
throw new SentryConfigError('tracesSampleRate must be between 0 and 1');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (config.maxBreadcrumbs !== undefined) {
|
|
34
|
+
if (config.maxBreadcrumbs < 0 || config.maxBreadcrumbs > 200) {
|
|
35
|
+
throw new SentryConfigError('maxBreadcrumbs must be between 0 and 200');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentry Initializer
|
|
3
|
+
* Single Responsibility: Initialize Sentry with validated configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SentryClient } from '../config/SentryClient';
|
|
7
|
+
import { SentryConfigValidator } from '../config/validators/SentryConfigValidator';
|
|
8
|
+
import type { SentryConfig } from '../../domain/entities/SentryConfig';
|
|
9
|
+
|
|
10
|
+
export async function initializeSentry(config: SentryConfig): Promise<void> {
|
|
11
|
+
SentryConfigValidator.validate(config);
|
|
12
|
+
await SentryClient.initialize(config);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isSentryInitialized(): boolean {
|
|
16
|
+
return SentryClient.isInitialized();
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useBreadcrumb Hook
|
|
3
|
+
* Single Responsibility: Provide breadcrumb logging functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from 'react';
|
|
7
|
+
import { SentryClient } from '../../infrastructure/config/SentryClient';
|
|
8
|
+
import type { Breadcrumb } from '../../domain/value-objects/ErrorMetadata';
|
|
9
|
+
|
|
10
|
+
export interface UseBreadcrumbReturn {
|
|
11
|
+
addBreadcrumb: (breadcrumb: Breadcrumb) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useBreadcrumb(): UseBreadcrumbReturn {
|
|
15
|
+
const addBreadcrumb = useCallback((breadcrumb: Breadcrumb): void => {
|
|
16
|
+
SentryClient.addBreadcrumb(breadcrumb);
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
addBreadcrumb,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSentry Hook
|
|
3
|
+
* Single Responsibility: Provide error tracking functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from 'react';
|
|
7
|
+
import { SentryClient } from '../../infrastructure/config/SentryClient';
|
|
8
|
+
import type { CaptureMetadata, MessageLevel } from '../../application/ports/ISentryClient';
|
|
9
|
+
import type { UserData } from '../../domain/value-objects/ErrorMetadata';
|
|
10
|
+
|
|
11
|
+
export interface UseSentryReturn {
|
|
12
|
+
captureException: (error: Error, metadata?: CaptureMetadata) => Promise<string | undefined>;
|
|
13
|
+
captureMessage: (message: string, level?: MessageLevel, metadata?: CaptureMetadata) => Promise<string | undefined>;
|
|
14
|
+
setUser: (user: UserData | null) => void;
|
|
15
|
+
clearUser: () => void;
|
|
16
|
+
setTag: (key: string, value: string | number | boolean) => void;
|
|
17
|
+
setExtra: (key: string, value: any) => void;
|
|
18
|
+
isInitialized: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useSentry(): UseSentryReturn {
|
|
22
|
+
const captureException = useCallback(async (
|
|
23
|
+
error: Error,
|
|
24
|
+
metadata?: CaptureMetadata,
|
|
25
|
+
): Promise<string | undefined> => {
|
|
26
|
+
return await SentryClient.captureException(error, metadata);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const captureMessage = useCallback(async (
|
|
30
|
+
message: string,
|
|
31
|
+
level?: MessageLevel,
|
|
32
|
+
metadata?: CaptureMetadata,
|
|
33
|
+
): Promise<string | undefined> => {
|
|
34
|
+
return await SentryClient.captureMessage(message, level, metadata);
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const setUser = useCallback((user: UserData | null): void => {
|
|
38
|
+
SentryClient.setUser(user);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const clearUser = useCallback((): void => {
|
|
42
|
+
SentryClient.clearUser();
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const setTag = useCallback((key: string, value: string | number | boolean): void => {
|
|
46
|
+
SentryClient.setTag(key, value);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const setExtra = useCallback((key: string, value: any): void => {
|
|
50
|
+
SentryClient.setExtra(key, value);
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
captureException,
|
|
55
|
+
captureMessage,
|
|
56
|
+
setUser,
|
|
57
|
+
clearUser,
|
|
58
|
+
setTag,
|
|
59
|
+
setExtra,
|
|
60
|
+
isInitialized: SentryClient.isInitialized(),
|
|
61
|
+
};
|
|
62
|
+
}
|