Sentry 隐私与安全 #
为什么隐私与安全很重要? #
在错误监控过程中,可能会意外收集敏感信息,这会带来严重的法律和声誉风险。
text
┌─────────────────────────────────────────────────────────────┐
│ 隐私与安全风险 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 敏感数据泄露 │
│ - 用户密码、信用卡号 │
│ - 个人身份信息 (PII) │
│ - API 密钥、令牌 │
│ │
│ 2. 合规性问题 │
│ - GDPR(欧盟) │
│ - CCPA(加州) │
│ - HIPAA(医疗) │
│ │
│ 3. 安全风险 │
│ - 凭证泄露 │
│ - 内部信息暴露 │
│ - 攻击面扩大 │
│ │
└─────────────────────────────────────────────────────────────┘
数据脱敏 #
beforeSend 过滤 #
javascript
// JavaScript - 过滤敏感数据
Sentry.init({
dsn: "https://xxxxxxxx@o123456.ingest.sentry.io/1234567",
beforeSend(event, hint) {
// 过滤请求体中的敏感字段
if (event.request?.data) {
event.request.data = sanitizeData(event.request.data);
}
// 过滤请求头中的敏感信息
if (event.request?.headers) {
delete event.request.headers.authorization;
delete event.request.headers.cookie;
}
// 过滤 URL 中的敏感参数
if (event.request?.url) {
event.request.url = sanitizeUrl(event.request.url);
}
// 过滤面包屑
if (event.breadcrumbs) {
event.breadcrumbs = event.breadcrumbs.map(sanitizeBreadcrumb);
}
return event;
},
});
function sanitizeData(data) {
const sensitiveFields = [
"password",
"passwd",
"secret",
"token",
"apiKey",
"api_key",
"creditCard",
"credit_card",
"ssn",
"socialSecurityNumber",
];
if (typeof data === "object" && data !== null) {
const sanitized = { ...data };
for (const field of sensitiveFields) {
if (field in sanitized) {
sanitized[field] = "[Filtered]";
}
}
// 递归处理嵌套对象
for (const key in sanitized) {
if (typeof sanitized[key] === "object") {
sanitized[key] = sanitizeData(sanitized[key]);
}
}
return sanitized;
}
return data;
}
function sanitizeUrl(url) {
// 移除 URL 中的敏感参数
const sensitiveParams = ["token", "key", "secret", "password"];
const urlObj = new URL(url);
for (const param of sensitiveParams) {
if (urlObj.searchParams.has(param)) {
urlObj.searchParams.set(param, "[Filtered]");
}
}
return urlObj.toString();
}
Python 数据脱敏 #
python
import sentry_sdk
import re
SENSITIVE_FIELDS = {
"password",
"passwd",
"secret",
"token",
"api_key",
"credit_card",
"ssn",
}
def sanitize_data(data):
if isinstance(data, dict):
return {
key: "[Filtered]" if key.lower() in SENSITIVE_FIELDS else sanitize_data(value)
for key, value in data.items()
}
elif isinstance(data, list):
return [sanitize_data(item) for item in data]
elif isinstance(data, str):
# 脱敏信用卡号
data = re.sub(r"\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b", "[Filtered]", data)
# 脱敏邮箱
data = re.sub(r"\b[\w\.-]+@[\w\.-]+\.\w+\b", "[Filtered]", data)
# 脱敏 SSN
data = re.sub(r"\b\d{3}-\d{2}-\d{4}\b", "[Filtered]", data)
return data
def before_send(event, hint):
# 过滤请求数据
if "request" in event:
request = event["request"]
if "data" in request:
request["data"] = sanitize_data(request["data"])
if "headers" in request:
headers = request["headers"]
for header in ["authorization", "cookie", "set-cookie"]:
if header in headers:
headers[header] = "[Filtered]"
return event
sentry_sdk.init(
dsn="https://xxxxxxxx@o123456.ingest.sentry.io/1234567",
before_send=before_send,
)
正则表达式脱敏 #
javascript
// 常见敏感数据的正则表达式
const patterns = {
// 信用卡号
creditCard: /\b(?:\d{4}[- ]?){3}\d{4}\b/g,
// 邮箱
email: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g,
// SSN(美国社会安全号)
ssn: /\b\d{3}-\d{2}-\d{4}\b/g,
// API 密钥(常见格式)
apiKey: /\b[a-zA-Z0-9]{32,}\b/g,
// JWT Token
jwt: /\beyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*\b/g,
// 手机号(中国)
phoneCN: /\b1[3-9]\d{9}\b/g,
// 身份证号(中国)
idCardCN: /\b\d{17}[\dXx]\b/g,
};
function sanitizeString(str) {
let result = str;
for (const [name, pattern] of Object.entries(patterns)) {
result = result.replace(pattern, `[${name} Filtered]`);
}
return result;
}
用户隐私 #
最小化用户信息 #
javascript
// ✅ 好的做法:只收集必要的用户信息
Sentry.setUser({
id: user.id, // 唯一标识符
});
// ⚠️ 谨慎使用:可能包含 PII
Sentry.setUser({
id: user.id,
email: user.email, // 可选,谨慎使用
username: user.username, // 可选,谨慎使用
});
// ❌ 不要这样做:包含敏感信息
Sentry.setUser({
id: user.id,
email: user.email,
password: user.password, // 绝对不要!
ssn: user.ssn, // 绝对不要!
});
IP 地址处理 #
javascript
// 不收集 IP 地址
Sentry.init({
dsn: "https://xxxxxxxx@o123456.ingest.sentry.io/1234567",
sendDefaultPii: false, // 不发送默认 PII
});
// 或者手动设置 IP
Sentry.init({
dsn: "https://xxxxxxxx@o123456.ingest.sentry.io/1234567",
beforeSend(event) {
if (event.request) {
// 移除 IP 地址
delete event.request.headers?.["x-forwarded-for"];
delete event.request.headers?.["x-real-ip"];
}
return event;
},
});
用户同意 #
javascript
// 在用户同意后才初始化 Sentry
function initSentryIfConsented() {
const hasConsent = localStorage.getItem("analyticsConsent") === "true";
if (hasConsent) {
Sentry.init({
dsn: "https://xxxxxxxx@o123456.ingest.sentry.io/1234567",
});
}
}
// 用户同意后
function onUserConsent() {
localStorage.setItem("analyticsConsent", "true");
initSentryIfConsented();
}
// 用户撤回同意
function onUserRevokeConsent() {
localStorage.setItem("analyticsConsent", "false");
// 关闭 Sentry
Sentry.close();
}
Session Replay 隐私 #
配置隐私选项 #
javascript
Sentry.init({
dsn: "https://xxxxxxxx@o123456.ingest.sentry.io/1234567",
integrations: [
Sentry.replayIntegration({
// 遮罩所有文本
maskAllText: true,
// 阻止所有媒体
blockAllMedia: true,
// 自定义遮罩选择器
mask: [
".sensitive-data",
"[data-sentry-mask]",
"input[type='password']",
"input[type='email']",
"input[type='tel']",
],
// 自定义阻止选择器
block: [
".blocked-element",
"[data-sentry-block]",
"iframe",
"video",
"audio",
],
// 忽略特定元素
ignore: [
".ignore-element",
"[data-sentry-ignore]",
],
}),
],
});
HTML 元素标记 #
html
<!-- 遮罩文本(显示为灰色块) -->
<div data-sentry-mask>
<p>Credit Card: 4111-1111-1111-1111</p>
</div>
<!-- 完全阻止(不录制) -->
<div data-sentry-block>
<img src="profile.jpg" alt="Profile Photo">
</div>
<!-- 忽略(不录制交互) -->
<div data-sentry-ignore>
<canvas id="animation"></canvas>
</div>
网络请求隐私 #
javascript
Sentry.init({
integrations: [
Sentry.replayIntegration({
// 只捕获特定 URL 的请求详情
networkDetailAllowUrls: [/api\.example\.com/],
// 不捕获请求体和响应体
networkCaptureBodies: false,
// 脱敏请求头
networkRequestHeaders: ["content-type"],
networkResponseHeaders: ["content-type"],
}),
],
});
合规性 #
GDPR 合规 #
javascript
// GDPR 合规配置
Sentry.init({
dsn: "https://xxxxxxxx@o123456.ingest.sentry.io/1234567",
// 不发送默认 PII
sendDefaultPii: false,
// 数据处理
beforeSend(event) {
// 移除所有 PII
return removePII(event);
},
// 面包屑过滤
beforeBreadcrumb(breadcrumb) {
// 不记录可能包含 PII 的面包屑
if (breadcrumb.category === "http" && breadcrumb.data?.url?.includes("login")) {
return null;
}
return breadcrumb;
},
});
// 数据主体权利
// 1. 访问权:用户可以请求查看其数据
// 2. 删除权:用户可以请求删除其数据
// 3. 携带权:用户可以请求导出其数据
数据保留策略 #
yaml
# Sentry 数据保留配置(在 Sentry 控制台)
data_retention:
# 事件数据保留时间
event_retention: 30 # 天
# 附件保留时间
attachment_retention: 30 # 天
# Session Replay 保留时间
replay_retention: 30 # 天
# 性能数据保留时间
performance_retention: 30 # 天
数据处理协议 #
text
┌─────────────────────────────────────────────────────────────┐
│ 数据处理清单 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 数据最小化 │
│ □ 只收集必要的数据 │
│ □ 不收集敏感信息 │
│ │
│ 2. 数据脱敏 │
│ □ 实施数据脱敏策略 │
│ □ 定期审查脱敏规则 │
│ │
│ 3. 访问控制 │
│ □ 限制数据访问权限 │
│ □ 实施审计日志 │
│ │
│ 4. 数据保留 │
│ □ 设置合理的数据保留期限 │
│ □ 定期清理过期数据 │
│ │
│ 5. 用户权利 │
│ □ 提供数据访问机制 │
│ □ 提供数据删除机制 │
│ │
└─────────────────────────────────────────────────────────────┘
安全最佳实践 #
1. 保护 DSN #
javascript
// ❌ 不要在客户端暴露私有 DSN
// 私有 DSN 应该只用于服务端
// ✅ 使用公开 DSN(前端)
Sentry.init({
dsn: "https://xxxxxxxx@o123456.ingest.sentry.io/1234567", // 公开 DSN
});
// ✅ 服务端使用私有 DSN
// 通过环境变量管理
Sentry.init({
dsn: process.env.SENTRY_DSN,
});
2. 限制域名 #
javascript
// 在 Sentry 项目设置中限制允许的域名
// Settings > Projects > [Project] > Client Keys (DSN) > Domain Whitelist
// 只允许特定域名发送事件
allowedDomains: [
"example.com",
"*.example.com",
]
3. 使用环境变量 #
javascript
// ✅ 使用环境变量
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
});
// ❌ 不要硬编码
Sentry.init({
dsn: "https://xxxxxxxx@o123456.ingest.sentry.io/1234567", // 硬编码
});
4. 审计日志 #
javascript
// 记录敏感操作
function sensitiveOperation(data) {
Sentry.addBreadcrumb({
category: "security",
message: "Sensitive operation performed",
level: "info",
data: {
operation: "data_export",
userId: data.userId,
timestamp: new Date().toISOString(),
},
});
// 执行操作...
}
5. 定期审查 #
markdown
## 安全审查清单
### 每月审查
- [ ] 检查是否有敏感数据泄露
- [ ] 审查用户权限设置
- [ ] 检查告警配置
### 每季度审查
- [ ] 更新数据脱敏规则
- [ ] 审查数据保留策略
- [ ] 检查合规性要求
### 每年审查
- [ ] 全面安全审计
- [ ] 更新隐私政策
- [ ] 员工安全培训
安全事件响应 #
发现数据泄露时 #
text
┌─────────────────────────────────────────────────────────────┐
│ 安全事件响应流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 立即行动 │
│ ├── 识别泄露的数据类型和范围 │
│ ├── 隔离受影响的系统 │
│ └── 通知安全团队 │
│ │
│ 2. 缓解措施 │
│ ├── 删除 Sentry 中的敏感数据 │
│ ├── 更新脱敏规则 │
│ └── 重新生成泄露的凭证 │
│ │
│ 3. 通知 │
│ ├── 通知受影响的用户 │
│ ├── 通知监管机构(如需要) │
│ └── 记录事件 │
│ │
│ 4. 后续 │
│ ├── 根本原因分析 │
│ ├── 改进安全措施 │
│ └── 更新培训材料 │
│ │
└─────────────────────────────────────────────────────────────┘
删除敏感数据 #
bash
# 使用 Sentry CLI 删除敏感事件
sentry-cli events delete EVENT_ID
# 删除整个 Issue
sentry-cli issues delete ISSUE_ID
# 批量删除(通过 API)
curl -X DELETE \
-H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
"https://sentry.io/api/0/projects/org/project/events/?query=sensitive_data"
总结 #
隐私与安全检查清单 #
markdown
## 实施前检查
- [ ] 配置 beforeSend 过滤敏感数据
- [ ] 配置 beforeBreadcrumb 过滤敏感面包屑
- [ ] 设置 sendDefaultPii: false
- [ ] 配置 Session Replay 隐私选项
- [ ] 审查用户信息收集范围
- [ ] 设置数据保留策略
- [ ] 配置域名白名单
- [ ] 使用环境变量管理配置
- [ ] 准备用户同意机制
- [ ] 制定安全事件响应计划
下一步 #
恭喜你完成了 Sentry 文档的学习!现在你已经掌握了从入门到专家的 Sentry 知识,可以开始在实际项目中应用了。
最后更新:2026-03-29