Skip to content

Commit f8cd421

Browse files
authored
feat: implement consent-aware Sentry error monitoring
2 parents cfd8467 + 7e73595 commit f8cd421

File tree

7 files changed

+165
-0
lines changed

7 files changed

+165
-0
lines changed

ThingConnect.Pulse.Server/Program.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ public static async Task Main(string[] args)
4040
// Use Serilog as the logging provider
4141
builder.Host.UseSerilog();
4242

43+
// NOTE: Sentry initialization removed - will be initialized conditionally
44+
// based on user consent in the ConsentAwareSentryService
45+
4346
// Configure Windows Service hosting
4447
builder.Host.UseWindowsService();
4548

@@ -132,6 +135,9 @@ public static async Task Main(string[] args)
132135
// Add path service
133136
builder.Services.AddSingleton<IPathService, PathService>();
134137

138+
// Add consent-aware Sentry service
139+
builder.Services.AddSingleton<IConsentAwareSentryService, ConsentAwareSentryService>();
140+
135141

136142
// Add configuration services
137143
builder.Services.AddScoped<ConfigurationParser>(serviceProvider =>
@@ -208,6 +214,14 @@ public static async Task Main(string[] args)
208214
Log.Information("Sample configuration initialization completed");
209215
}
210216

217+
// Initialize Sentry based on user consent
218+
using (IServiceScope scope = app.Services.CreateScope())
219+
{
220+
IConsentAwareSentryService sentryService = scope.ServiceProvider.GetRequiredService<IConsentAwareSentryService>();
221+
await sentryService.InitializeIfConsentedAsync();
222+
Log.Information("Consent-aware Sentry initialization completed");
223+
}
224+
211225
app.UseDefaultFiles();
212226
app.UseStaticFiles();
213227

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using Sentry;
2+
3+
namespace ThingConnect.Pulse.Server.Services;
4+
5+
public interface IConsentAwareSentryService
6+
{
7+
Task InitializeIfConsentedAsync();
8+
void CaptureException(Exception exception);
9+
void CaptureMessage(string message, SentryLevel level = SentryLevel.Info);
10+
}
11+
12+
public class ConsentAwareSentryService : IConsentAwareSentryService
13+
{
14+
private readonly IServiceProvider _serviceProvider;
15+
private readonly ILogger<ConsentAwareSentryService> _logger;
16+
private bool _sentryInitialized = false;
17+
private bool _consentChecked = false;
18+
19+
public ConsentAwareSentryService(IServiceProvider serviceProvider, ILogger<ConsentAwareSentryService> logger)
20+
{
21+
_serviceProvider = serviceProvider;
22+
_logger = logger;
23+
}
24+
25+
public async Task InitializeIfConsentedAsync()
26+
{
27+
if (_consentChecked) return;
28+
29+
try
30+
{
31+
using var scope = _serviceProvider.CreateScope();
32+
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
33+
34+
// Check if user has consented to error diagnostics
35+
string? errorDiagnosticsConsent = await settingsService.GetAsync("telemetry_error_diagnostics");
36+
bool hasErrorDiagnosticsConsent = bool.TryParse(errorDiagnosticsConsent, out bool errorValue) && errorValue;
37+
38+
_consentChecked = true;
39+
40+
if (hasErrorDiagnosticsConsent && !_sentryInitialized)
41+
{
42+
SentrySdk.Init(options =>
43+
{
44+
options.Dsn = "https://[email protected]/4510000957882368";
45+
// Privacy-first configuration: no PII data collection
46+
options.SendDefaultPii = false;
47+
// Disable debug mode for production privacy
48+
options.Debug = false;
49+
// Note: Additional PII filtering would be done here if needed
50+
// but SendDefaultPii = false already handles most privacy concerns
51+
});
52+
53+
_sentryInitialized = true;
54+
_logger.LogInformation("Sentry initialized with user consent for error diagnostics");
55+
}
56+
else
57+
{
58+
_logger.LogInformation("Sentry not initialized - no user consent for error diagnostics");
59+
}
60+
}
61+
catch (Exception ex)
62+
{
63+
_logger.LogWarning(ex, "Failed to check telemetry consent for Sentry initialization");
64+
}
65+
}
66+
67+
public void CaptureException(Exception exception)
68+
{
69+
if (_sentryInitialized)
70+
{
71+
SentrySdk.CaptureException(exception);
72+
}
73+
}
74+
75+
public void CaptureMessage(string message, SentryLevel level = SentryLevel.Info)
76+
{
77+
if (_sentryInitialized)
78+
{
79+
SentrySdk.CaptureMessage(message, level);
80+
}
81+
}
82+
}

ThingConnect.Pulse.Server/ThingConnect.Pulse.Server.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
2424
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.8" />
2525
<PackageReference Include="NJsonSchema" Version="11.4.0" />
26+
<PackageReference Include="Sentry" Version="5.15.0" />
27+
<PackageReference Include="Sentry.AspNetCore" Version="5.15.0" />
2628
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
2729
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
2830
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />

thingconnect.pulse.client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@emotion/react": "^11.14.0",
1717
"@hookform/resolvers": "^5.2.1",
1818
"@monaco-editor/react": "^4.7.0",
19+
"@sentry/react": "^10.11.0",
1920
"@tanstack/react-query": "^5.84.2",
2021
"axios": "^1.11.0",
2122
"date-fns": "^4.1.0",

thingconnect.pulse.client/src/features/auth/context/AuthContext.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type LoginRequest,
77
type RegisterRequest,
88
} from '../services/authService';
9+
import { useSentryConsentInit } from '../../../hooks/useSentryConsentInit';
910

1011
interface AuthContextType {
1112
user: UserInfo | null;
@@ -31,6 +32,9 @@ export function AuthProvider({ children }: AuthProviderProps) {
3132

3233
const isAuthenticated = user !== null;
3334

35+
// Initialize Sentry based on user consent when authenticated
36+
useSentryConsentInit(isAuthenticated);
37+
3438
const checkSession = async () => {
3539
try {
3640
setIsLoading(true);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useEffect, useState } from 'react';
2+
import * as Sentry from "@sentry/react";
3+
4+
let sentryInitialized = false;
5+
6+
export function useSentryConsentInit(isAuthenticated: boolean) {
7+
const [initializationAttempted, setInitializationAttempted] = useState(false);
8+
9+
useEffect(() => {
10+
const initializeSentryIfConsented = async () => {
11+
// Only attempt initialization once and when user is authenticated
12+
if (!isAuthenticated || initializationAttempted || sentryInitialized) {
13+
return;
14+
}
15+
16+
setInitializationAttempted(true);
17+
18+
try {
19+
// Check if user has consented to error diagnostics
20+
const consentResponse = await fetch('/api/auth/telemetry-consent', {
21+
credentials: 'include' // Include cookies for authentication
22+
});
23+
24+
if (consentResponse.ok) {
25+
const consent = await consentResponse.json() as { errorDiagnostics: boolean; usageAnalytics: boolean };
26+
27+
if (consent.errorDiagnostics) {
28+
Sentry.init({
29+
dsn: "https://[email protected]/4510005218443264",
30+
// Privacy-first configuration: no PII data collection
31+
sendDefaultPii: false,
32+
beforeSend(event) {
33+
// Additional privacy filtering - remove any potential PII
34+
if (event.user) {
35+
delete event.user.ip_address;
36+
delete event.user.email;
37+
}
38+
return event;
39+
}
40+
});
41+
42+
sentryInitialized = true;
43+
console.log('Sentry initialized with user consent for error diagnostics');
44+
} else {
45+
console.log('Sentry not initialized - user has not consented to error diagnostics');
46+
}
47+
} else {
48+
console.log('Sentry not initialized - could not verify consent');
49+
}
50+
} catch (error) {
51+
console.log('Sentry initialization skipped - consent verification failed:', error);
52+
}
53+
};
54+
55+
void initializeSentryIfConsented();
56+
}, [isAuthenticated, initializationAttempted]);
57+
58+
return { sentryInitialized };
59+
}

thingconnect.pulse.client/src/main.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { StrictMode } from 'react';
55
import { createRoot } from 'react-dom/client';
66
import App from './App.tsx';
77

8+
// NOTE: Sentry initialization moved to useSentryConsentInit hook
9+
// This ensures Sentry is only initialized after authentication and with explicit consent
10+
811
createRoot(document.getElementById('root')!).render(
912
<StrictMode>
1013
<QueryProvider>

0 commit comments

Comments
 (0)