Storage · DOM 스냅샷
opt-in 으로 localStorage · cookies · DOM 을 수집하고, 비밀번호·토큰은 클라이언트에서 마스킹합니다.
이슈 캡처 시점의 localStorage, sessionStorage, cookies, DOM outerHTML 을 함께 박제합니다. 재현이 어려운 상태 의존 버그(로그인 토큰 만료·feature flag 분기·클라이언트 캐시 불일치 등)에서 결정적인 단서가 됩니다. 동시에 민감 정보 노출 위험이 크기 때문에 기본 off · 프로젝트별 명시 활성화 로만 켭니다.
왜 opt-in 인가
localStorage 와 cookies 는 통상 인증 토큰 · 사용자 ID · feature flag 를 포함합니다. DOM 에는 입력 중인 비밀번호 · 결제 카드 정보 · 다른 사용자의 PII 가 노출될 수 있습니다. 이 데이터를 무차별 수집하면 한 번의 이슈 리포트가 곧 보안 사고가 됩니다.
따라서 QA Note 는 다음 원칙을 따릅니다.
- 기본 off — 프로젝트 생성 시 Storage / DOM 캡처는 비활성화 상태입니다.
- 프로젝트별 활성화 — 조직 단위가 아니라 프로젝트 단위로 켭니다.
- 클라이언트 마스킹 우선 — 마스킹은 Extension / Widget 측에서 먼저 수행됩니다. 서버에 도달하기 전에 비밀번호 · 토큰이 제거됩니다.
- 수집 범위 명시 — 캡처 직전에 사용자에게 "이 이슈에 storage / DOM 이 포함됩니다" 를 알립니다.
활성화 방법
대시보드에서 다음 경로로 진입합니다.
프로젝트 설정 → 기술 수집 옵션 → Storage / DOM 토글 on
경로: /settings/projects/[projectId]/capture
토글을 켜면 이후 생성되는 이슈부터 수집이 시작됩니다. 이미 만들어진 이슈에는 소급 적용되지 않습니다. 끄면 다음 이슈부터 미수집되고, 기존 이슈의 수집된 데이터는 별도 삭제 API 를 호출해야 제거됩니다(§해제·삭제).
수집되는 데이터
| 항목 | 형식 | 크기 한도 | 마스킹 적용 |
|---|---|---|---|
localStorage | Record<string, string> | 키당 2KB · 합계 50KB | ✅ |
sessionStorage | Record<string, string> | 키당 2KB · 합계 50KB | ✅ |
cookies | Array<{ name, value, domain }> | 합계 30KB | ✅ |
DOM outerHTML | string (gzip 압축) | 압축 후 500KB | ✅ |
한도를 넘는 값은 끝부분이 잘리고 ... [truncated] 마커가 붙습니다. DOM 은 gzip 압축 후 크기로 판정합니다.
마스킹 SSoT — 14 + 13 + 14
마스킹 규칙은 한 모듈에 SSoT 로 정의되어 있습니다.
실제 필터 정의:
- apps/extension/src/lib/masking/patterns.ts (extension)
- apps/web/src/lib/capture/masking.ts (server-side reinforcement, 향후)
세 필터 세트의 항목 수는 다음과 같습니다.
| 세트 | 항목 수 | 예시 |
|---|---|---|
| URL · 쿼리 파라미터 | 14 | password, token, access_token, refresh_token, secret, api_key, apikey, auth, authorization, session, sessionid, csrf, state, code |
| HTTP 헤더 | 13 | Authorization, Cookie, Set-Cookie, X-Api-Key, X-Auth-Token, X-Csrf-Token, X-Session-Id, Proxy-Authorization, Www-Authenticate, X-Access-Token, X-Refresh-Token, X-Forwarded-For, X-Real-IP |
| 요청·응답 body 필드 | 14 | password, password_confirmation, currentPassword, newPassword, token, accessToken, refreshToken, secret, apiKey, creditCard, cardNumber, cvv, ssn, pin |
각 세트는 대소문자 무관 정확 매치 + 부분 문자열 매치 (예: *token*) 두 패턴을 함께 사용합니다.
매칭된 값은 길이 정보만 보존되고 내용은 *** 으로 치환됩니다 (Authorization: Bearer eyJ... → Authorization: ***).
DOM 마스킹 규칙
DOM outerHTML 은 다음 규칙으로 정제 후 저장됩니다.
| 규칙 | 처리 |
|---|---|
<script> 태그 | 통째로 제거 (XSS · 토큰 인라인 노출 방지) |
<input type="password"> | value 속성 제거 |
<input type="email">, <input type="tel"> | 기본은 보존, 프로젝트 옵션으로 제거 가능 |
data-password, data-token, data-secret, data-sensitive | 속성 제거 |
| 파일 업로드 input | value 제거 (파일명 노출 방지) |
iframe 내부 DOM 은 same-origin 일 때만 수집됩니다. cross-origin iframe 은 placeholder 만 남고 내부는 비웁니다.
MCP 에서 조회
두 도구로 조회합니다.
get_storage → localStorage · sessionStorage · cookies (마스킹 적용 후)
get_dom_snapshot → DOM outerHTML (gzip 압축 해제 후 반환)
get_storage 응답 예시:
{
"localStorage": {
"user_id": "usr_123",
"auth_token": "***",
"feature_flags": "{\"new_billing\":true}"
},
"sessionStorage": {
"draft_issue": "..."
},
"cookies": [
{ "name": "session", "value": "***", "domain": ".qanote.app" }
]
}
마스킹 적용 후의 결과만 반환되므로 LLM 컨텍스트로 그대로 던져도 안전합니다.
저장 형식 · 크기
- 압축: DOM
outerHTML은 gzip 압축 후 R2 에 저장됩니다 (압축률 평균 80% 이상). - 로딩: 이슈 상세 페이지는 압축 데이터를 그대로 전송받아 클라이언트에서 압축 해제합니다.
- 보존 기간: 이슈와 동일한 보존 기간을 따릅니다 (플랜별 설정).
- 합계 크기: 한 이슈당 storage + DOM 합계 600KB 미만이 일반적입니다.
해제 · 삭제
- 이후 이슈부터 미수집: 프로젝트 설정에서 토글 off → 다음 이슈부터 storage / DOM 비포함.
- 기존 데이터 삭제: 기존에 수집된 데이터는 토글 off 만으로 사라지지 않습니다. 다음 API 또는 대시보드 액션이 필요합니다.
# 프로젝트의 모든 이슈에서 storage · DOM 데이터 일괄 삭제
DELETE /api/projects/[projectId]/captured-data?types=storage,dom
# 단일 이슈에서 삭제
DELETE /api/issues/[issueId]/captured-data?types=storage,dom
대시보드에서는 프로젝트 설정 → "수집 데이터 일괄 삭제" 버튼으로 동일 작업을 수행합니다.