@abto-app/event 0.1.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/LICENSE +21 -0
- package/README.md +273 -0
- package/dist/autocapture.d.ts +68 -0
- package/dist/autocapture.d.ts.map +1 -0
- package/dist/autocapture.js +269 -0
- package/dist/autocapture.js.map +1 -0
- package/dist/click-signals.d.ts +18 -0
- package/dist/click-signals.d.ts.map +1 -0
- package/dist/click-signals.js +66 -0
- package/dist/click-signals.js.map +1 -0
- package/dist/client.d.ts +37 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +439 -0
- package/dist/client.js.map +1 -0
- package/dist/context.d.ts +52 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +146 -0
- package/dist/context.js.map +1 -0
- package/dist/elements-chain.d.ts +8 -0
- package/dist/elements-chain.d.ts.map +1 -0
- package/dist/elements-chain.js +84 -0
- package/dist/elements-chain.js.map +1 -0
- package/dist/event-registry.d.ts +37 -0
- package/dist/event-registry.d.ts.map +1 -0
- package/dist/event-registry.js +59 -0
- package/dist/event-registry.js.map +1 -0
- package/dist/events.generated.d.ts +769 -0
- package/dist/events.generated.d.ts.map +1 -0
- package/dist/events.generated.js +3 -0
- package/dist/events.generated.js.map +1 -0
- package/dist/identity.d.ts +44 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +148 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/privacy.d.ts +46 -0
- package/dist/privacy.d.ts.map +1 -0
- package/dist/privacy.js +140 -0
- package/dist/privacy.js.map +1 -0
- package/dist/scroll-depth.d.ts +24 -0
- package/dist/scroll-depth.d.ts.map +1 -0
- package/dist/scroll-depth.js +74 -0
- package/dist/scroll-depth.js.map +1 -0
- package/dist/system-events.generated.d.ts +17 -0
- package/dist/system-events.generated.d.ts.map +1 -0
- package/dist/system-events.generated.js +29 -0
- package/dist/system-events.generated.js.map +1 -0
- package/dist/transport.d.ts +27 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +264 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +156 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/uuid.d.ts +12 -0
- package/dist/uuid.d.ts.map +1 -0
- package/dist/uuid.js +39 -0
- package/dist/uuid.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Greedy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# @abto-app/event
|
|
2
|
+
|
|
3
|
+
ABTO SDK는 브라우저에서 관측 가능한 사용자 행동을 수집하고, Gateway가 반환한 `request_id`를 통해 서버의 LLM 비용·지연 데이터와 연결한다.
|
|
4
|
+
|
|
5
|
+
`@abto-app/event`는 공개키를 사용하는 Browser 전용 패키지다.
|
|
6
|
+
비밀키와 provider credential을 사용하는 Node 환경에는 별도 `@abto-app/calling` 패키지를 설치한다.
|
|
7
|
+
|
|
8
|
+
PostHog의 autocapture, ingestion, session, schema/discovery는 ABTO 이벤트 설계의 참고 모델이다. ABTO 이벤트를 PostHog로 보내는 연동이 아니라, 검증된 수집 철학을 ABTO 독립 수집 구조에 적용한다.
|
|
9
|
+
|
|
10
|
+
## 설치
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add @abto-app/event
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 이벤트 경계
|
|
17
|
+
|
|
18
|
+
Browser SDK가 보내는 이벤트는 두 종류다.
|
|
19
|
+
|
|
20
|
+
| 종류 | SDK 이름 | Analytics wire 이름 | 정의 주체 | 발생 방식 |
|
|
21
|
+
|---|---|---|---|---|
|
|
22
|
+
| 시스템 이벤트 | `$`로 시작 | ABTO canonical 이름 | ABTO | SDK 자동 수집 또는 전용 API |
|
|
23
|
+
| 커스텀 이벤트 | `$` 없이 제품 도메인 이름 사용 | 같은 이름 | 고객 저장소 | `client.capture()` |
|
|
24
|
+
|
|
25
|
+
사용자는 `$` 이벤트나 `$` 속성을 등록할 수 없다. 아래 canonical wire 이름도 ABTO가 소유하므로 커스텀 이벤트로 등록할 수 없다. ABTO 시스템 이벤트는 일반 `capture()`로 보낼 수 없으며 SDK 내부 경로와 AI trace 전용 메서드만 발생시킨다.
|
|
26
|
+
|
|
27
|
+
### Browser SDK 시스템 이벤트
|
|
28
|
+
|
|
29
|
+
| SDK 이벤트 | Analytics `event_name` | 의미 | 발생 조건 |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| `$pageview` | `pageview` | 페이지/SPA route 진입 | 초기 로드, history 변경, bfcache 복원 |
|
|
32
|
+
| `$pageleave` | `pageleave` | 페이지/route 이탈 | SPA 이동, `pagehide` |
|
|
33
|
+
| `$autocapture` | `interaction_autocaptured` | DOM 상호작용 원시 사실 | click, change, submit, copy |
|
|
34
|
+
| `$rageclick` | `interaction_rageclick` | 짧은 시간의 반복 클릭 | SDK 휴리스틱 |
|
|
35
|
+
| `$dead_click` | `interaction_deadclick` | 반응이 관측되지 않은 클릭 | SDK 휴리스틱 |
|
|
36
|
+
| `$ai_prompt_submitted` | `llm_prompt_submitted` | 프롬프트 제출을 앱이 확인 | `trace.submitPrompt()` |
|
|
37
|
+
| `$ai_response_rendered` | `llm_response_rendered` | 응답이 UI에 렌더됨을 앱이 확인 | `trace.markResponseRendered()` |
|
|
38
|
+
| `$ai_response_interacted` | `llm_response_interacted` | 응답에 대한 명시적 행동 | `trace.captureResponseInteraction()` |
|
|
39
|
+
|
|
40
|
+
`$session_start`와 `$session_end`는 보내지 않는다. 모든 이벤트의 `$session_id`와 timestamp의 최솟값·최댓값을 분석 계층에서 사용해 세션 시작, 종료, duration을 파생한다. 브라우저 종료 신호는 유실될 수 있으므로 `$session_end`를 확정 사실로 기록하지 않는다.
|
|
41
|
+
|
|
42
|
+
## 커스텀 이벤트 정본: `abto.events.ts`
|
|
43
|
+
|
|
44
|
+
제품 이벤트는 고객 애플리케이션 저장소의 `abto.events.ts`에 사전 등록한다. 이 파일을 코드 리뷰와 향후 CLI/CI schema push의 정본으로 사용한다.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// abto.events.ts
|
|
48
|
+
import { defineEvents } from '@abto-app/event';
|
|
49
|
+
|
|
50
|
+
export const events = defineEvents({
|
|
51
|
+
checkout_completed: {
|
|
52
|
+
description: '결제가 완료됨',
|
|
53
|
+
properties: {
|
|
54
|
+
order_id: { type: 'string', required: true },
|
|
55
|
+
amount: { type: 'number', required: true },
|
|
56
|
+
currency: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
enum: ['KRW', 'USD'],
|
|
59
|
+
required: true,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { initAbto } from '@abto-app/event';
|
|
68
|
+
import { events } from './abto.events';
|
|
69
|
+
|
|
70
|
+
const abto = initAbto({
|
|
71
|
+
projectKey: 'public_project_key',
|
|
72
|
+
environment: 'development',
|
|
73
|
+
events,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
abto.capture('checkout_completed', {
|
|
77
|
+
order_id: 'order_123',
|
|
78
|
+
amount: 49_000,
|
|
79
|
+
currency: 'KRW',
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`defineEvents()`에서 타입을 추론하므로 잘못된 이벤트 이름, required 누락, enum 위반을 개발 시점에 확인할 수 있다. 런타임 정책은 환경별로 다르다.
|
|
84
|
+
|
|
85
|
+
| 환경 | 미등록 이벤트 | 등록 schema drift |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `development` | 전송하고 `Discovered` 경고 | 전송하고 drift 경고 |
|
|
88
|
+
| `production` | drop | required/type/enum 위반 drop |
|
|
89
|
+
|
|
90
|
+
알 수 없는 추가 속성은 막지 않는다. schema가 선언한 required/type/enum만 검사해 점진적 확장을 허용한다.
|
|
91
|
+
|
|
92
|
+
## 초기화와 autocapture
|
|
93
|
+
|
|
94
|
+
앱 루트에서 한 번 초기화하면 autocapture가 시작된다.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const abto = initAbto({
|
|
98
|
+
projectKey: 'public_project_key',
|
|
99
|
+
apiHost: 'https://api.abto.app',
|
|
100
|
+
environment: 'production',
|
|
101
|
+
events,
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
기본 endpoint는 `${apiHost}/v1/collect/events`다. SDK 내부의 Browser 이벤트는 전송 직전에
|
|
106
|
+
현재 Analytics 수신 계약으로 변환된다.
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"batch": [
|
|
111
|
+
{
|
|
112
|
+
"event_id": "019b...",
|
|
113
|
+
"device_id": "019b...",
|
|
114
|
+
"session_id": "019b...",
|
|
115
|
+
"event_name": "checkout_started",
|
|
116
|
+
"value": 3000,
|
|
117
|
+
"scale": "KRW",
|
|
118
|
+
"occurred_at": "2026-07-15T04:10:00.000Z",
|
|
119
|
+
"extra_json": {
|
|
120
|
+
"value": 3000,
|
|
121
|
+
"scale": "KRW",
|
|
122
|
+
"$lib": "web",
|
|
123
|
+
"$lib_version": "0.1.0"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
서버의 이벤트별 응답은 event UUID를 key로 사용한다.
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"results": {
|
|
135
|
+
"019b5b74-11d0-7000-8000-000000000001": {
|
|
136
|
+
"result": "drop",
|
|
137
|
+
"code": "schema_type_mismatch"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
모든 collector 요청은 public project key를 Bearer header에 싣는다. 페이지 이탈도 응답을 읽을 수 있는 `fetch(..., { keepalive: true })`를 사용하며, 서버가 이 key에서 `project_id`와 `account_id`를 결정한다. `$tenant_id`를 포함한 client property는 분석 문맥이며 인증·project 귀속 값이 아니다.
|
|
144
|
+
|
|
145
|
+
수신 계약의 상한은 요청당 100 events다. SDK 기본값은 20이며 약 60 KiB 이하 payload만 keepalive로 전송한다. malformed request와 인증 실패는 요청 단위 4xx, 개별 validation/storage 실패는 2xx 응답의 UUID별 `warning`, `drop`, `retry`로 처리한다.
|
|
146
|
+
커스텀 `event_name`은 Backend와 같은 UTF-16 기준 최대 200자이며, `defineEvents()`와 runtime capture가 enqueue 전에 검증한다.
|
|
147
|
+
metric `scale`은 최대 16자이며, 초과한 값은 event 전체가 drop되지 않도록 top-level metadata에서 제외한다.
|
|
148
|
+
|
|
149
|
+
SDK 내부 queue와 API에서는 `$` 이름을 유지하지만, Analytics의 고정 Biz Event 계약은 `$` 접두
|
|
150
|
+
`event_name`을 거절한다. Transport가 위 표의 canonical 이름으로만 변환해 전송하며 `$lib`,
|
|
151
|
+
`$device_id`, `$session_id` 같은 SDK 소유 context는 `extra_json`에 그대로 보존한다.
|
|
152
|
+
Dashboard 이벤트 카탈로그와 Success Metric에서는 canonical wire 이름을 사용한다.
|
|
153
|
+
|
|
154
|
+
annotation은 원시 `$autocapture`를 다른 이벤트로 바꾸지 않는다. 원시 상호작용을 보존하면서 분석 차원만 보강한다.
|
|
155
|
+
|
|
156
|
+
```html
|
|
157
|
+
<button
|
|
158
|
+
data-abto-action="accept"
|
|
159
|
+
data-abto-surface="generator"
|
|
160
|
+
data-abto-node-key="resume.make"
|
|
161
|
+
data-abto-response-id="resp_123"
|
|
162
|
+
data-abto-request-id="req_123">
|
|
163
|
+
적용
|
|
164
|
+
</button>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
위 클릭은 `$autocapture`로 수집되며 `$ai_action`, `$surface`, `$node_key`, `$response_id`, `$request_id`가 함께 실린다. 업무 의미가 확정된 행동은 앱 코드에서 커스텀 이벤트 또는 AI 전용 메서드로 별도 기록한다.
|
|
168
|
+
|
|
169
|
+
## 개인정보 기본값
|
|
170
|
+
|
|
171
|
+
prompt, response, DOM text/value는 기본적으로 원문을 수집하지 않는다.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
initAbto({
|
|
175
|
+
projectKey: 'public_project_key',
|
|
176
|
+
events,
|
|
177
|
+
capture: {
|
|
178
|
+
prompt: 'metadata_only',
|
|
179
|
+
response: 'metadata_only',
|
|
180
|
+
mask: 'all',
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
위 값들이 생략됐을 때도 같은 안전한 기본값이 적용된다.
|
|
186
|
+
|
|
187
|
+
| annotation | 동작 |
|
|
188
|
+
|---|---|
|
|
189
|
+
| `data-abto-no-capture` | 자신과 하위 트리를 수집하지 않음 |
|
|
190
|
+
| `data-abto-sensitive` | 자신과 하위 text/value를 항상 전체 마스킹 |
|
|
191
|
+
| `data-abto-include` | 해당 요소의 text/value 수집을 명시적으로 허용 |
|
|
192
|
+
|
|
193
|
+
`password`, `hidden` input과 카드·비밀번호·SSN 계열 필드는 annotation과 무관하게 보호한다. `full` 원문 수집은 명시적 opt-in이며 고객의 동의·보존·삭제 정책과 함께 사용해야 한다.
|
|
194
|
+
|
|
195
|
+
## 브라우저에서 관측 가능한 AI 이벤트
|
|
196
|
+
|
|
197
|
+
브라우저가 확실히 아는 세 가지 사실만 전용 API로 제공한다.
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const trace = abto.startLlmTrace({
|
|
201
|
+
nodeId: 'resume.make',
|
|
202
|
+
taskType: 'draft_generation',
|
|
203
|
+
surface: 'editor',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await trace.submitPrompt({
|
|
207
|
+
prompt: promptText,
|
|
208
|
+
language: 'ko',
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const response = await fetch('/api/generate', {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
headers: { 'content-type': 'application/json', ...trace.getHeaders() },
|
|
214
|
+
body: JSON.stringify({ prompt: promptText }),
|
|
215
|
+
});
|
|
216
|
+
trace.attachRequestId(response);
|
|
217
|
+
|
|
218
|
+
await trace.markResponseRendered({
|
|
219
|
+
responseId: 'resp_123',
|
|
220
|
+
timeToRenderMs: 1_380,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await trace.captureResponseInteraction('copied', {
|
|
224
|
+
responseId: 'resp_123',
|
|
225
|
+
source: 'copy_button',
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
provider/model/token/cost/retry/fallback, 실제 첫 토큰 시점과 request 성공·실패는 Server SDK/Gateway가 소유한다. AI task 완료·이탈은 제품마다 의미가 다르므로 커스텀 이벤트 또는 분석 파생 지표로 둔다.
|
|
230
|
+
|
|
231
|
+
## 식별자와 세션
|
|
232
|
+
|
|
233
|
+
| 속성 | 수명과 역할 |
|
|
234
|
+
|---|---|
|
|
235
|
+
| `$device_id` | 프로젝트별 브라우저 설치, localStorage 유지 |
|
|
236
|
+
| `$anonymous_id` | 로그인 전 distinct identity |
|
|
237
|
+
| `$user_id` | `identify()`로 연결한 제품 사용자 |
|
|
238
|
+
| `$session_id` | 탭 사이에서 공유하는 논리 세션, 30분 idle 또는 24시간 max age에 회전 |
|
|
239
|
+
| `$window_id` | 탭/window별 ID, sessionStorage 유지 |
|
|
240
|
+
| `$pageview_id` | 페이지/SPA route 구간, pageview마다 회전 |
|
|
241
|
+
| `$trace_id` | 한 사용자 행동에서 서버 호출까지 연결 |
|
|
242
|
+
| `$request_id` | Gateway의 실제 provider 호출 PK |
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
abto.identify('user_123', 'tenant_123');
|
|
246
|
+
abto.reset(); // user/tenant 제거, device 유지
|
|
247
|
+
abto.forgetDevice(); // outbox와 device identity 제거
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## 전송과 재시도
|
|
251
|
+
|
|
252
|
+
- 이벤트는 localStorage outbox에 먼저 저장한다.
|
|
253
|
+
- 기본적으로 최대 20개씩 `POST /v1/collect/events`로 보낸다.
|
|
254
|
+
- 일반 flush와 페이지 이탈 모두 응답 가능한 `fetch`를 사용하며, 안전 크기의 이탈 payload에만 `keepalive`를 켠다.
|
|
255
|
+
- keepalive payload는 약 60 KiB 이내로 제한한다. 응답 전에 페이지가 종료되면 localStorage outbox가 다음 SDK 인스턴스에서 재전송한다.
|
|
256
|
+
- 408, 429, 5xx와 이벤트별 `retry`만 지수 backoff로 재시도한다.
|
|
257
|
+
- 영구 4xx와 이벤트별 `drop`은 outbox에서 제거한다.
|
|
258
|
+
- 이벤트별 `ok`, `warning`, `drop`, `retry` 응답을 UUID 기준으로 처리한다.
|
|
259
|
+
|
|
260
|
+
## Public API (browser)
|
|
261
|
+
|
|
262
|
+
`initAbto` · `defineEvents` · `identify` · `getIdentity` · `reset` · `forgetDevice` · `startLlmTrace` · `setNode` · `getTraceHeaders` · `client.capture` · `trace.submitPrompt` · `trace.markResponseRendered` · `trace.captureResponseInteraction` · `flush`.
|
|
263
|
+
|
|
264
|
+
## 개발 검증
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
pnpm test
|
|
268
|
+
pnpm typecheck
|
|
269
|
+
pnpm build
|
|
270
|
+
node ../../examples/browser-smoke/collector.mjs
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
실브라우저 검증 절차는 [`examples/browser-smoke/README.md`](../../examples/browser-smoke/README.md)를 따른다.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broad, delegation-based interaction autocapture. data-abto-* annotations enrich
|
|
3
|
+
* the raw event; they never replace it with a guessed semantic event.
|
|
4
|
+
* SPA route changes emit pageview/pageleave. Session replay (rrweb) is out of scope.
|
|
5
|
+
*/
|
|
6
|
+
import { type ScrollDepthSnapshot } from './scroll-depth.js';
|
|
7
|
+
import type { AIInteractionType, AutocaptureEventType, MaskMode } from './types.js';
|
|
8
|
+
export declare const ATTR: {
|
|
9
|
+
readonly action: "data-abto-action";
|
|
10
|
+
readonly surface: "data-abto-surface";
|
|
11
|
+
readonly nodeId: "data-abto-node-key";
|
|
12
|
+
readonly requestId: "data-abto-request-id";
|
|
13
|
+
readonly conversationId: "data-abto-conversation-id";
|
|
14
|
+
readonly messageId: "data-abto-message-id";
|
|
15
|
+
readonly responseId: "data-abto-response-id";
|
|
16
|
+
readonly templateId: "data-abto-template-id";
|
|
17
|
+
};
|
|
18
|
+
export interface SemanticTarget {
|
|
19
|
+
action: string;
|
|
20
|
+
surface?: string;
|
|
21
|
+
node_key?: string;
|
|
22
|
+
request_id?: string;
|
|
23
|
+
conversation_id?: string;
|
|
24
|
+
message_id?: string;
|
|
25
|
+
response_id?: string;
|
|
26
|
+
template_id?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface CapturedElement {
|
|
29
|
+
tag: string;
|
|
30
|
+
text?: string;
|
|
31
|
+
value?: string;
|
|
32
|
+
input_type?: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
href?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface InteractionHit {
|
|
37
|
+
kind: 'interaction';
|
|
38
|
+
eventType: AutocaptureEventType;
|
|
39
|
+
target: SemanticTarget;
|
|
40
|
+
elementsChain: string;
|
|
41
|
+
element: CapturedElement;
|
|
42
|
+
selectionLength?: number;
|
|
43
|
+
}
|
|
44
|
+
export interface PageviewHit {
|
|
45
|
+
kind: 'pageview';
|
|
46
|
+
eventType: 'pageview' | 'pageleave';
|
|
47
|
+
path: string;
|
|
48
|
+
referrer?: string;
|
|
49
|
+
/** 떠나는 페이지의 체류 시간(wall-clock). pageleave 에만 실린다. */
|
|
50
|
+
durationMs?: number;
|
|
51
|
+
/** 떠나는 페이지의 스크롤 깊이. pageleave 에만 실린다. */
|
|
52
|
+
scroll?: ScrollDepthSnapshot;
|
|
53
|
+
/** 실제 페이지 언로드(pagehide)에서 발생한 pageleave. 수신측은 즉시 flush 해야 유실이 없다. */
|
|
54
|
+
unload?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface SignalHit {
|
|
57
|
+
kind: 'signal';
|
|
58
|
+
eventType: 'rageclick' | 'deadclick';
|
|
59
|
+
elementsChain: string;
|
|
60
|
+
clickCount?: number;
|
|
61
|
+
}
|
|
62
|
+
export type AutocaptureHit = InteractionHit | PageviewHit | SignalHit;
|
|
63
|
+
export type AutocaptureSink = (hit: AutocaptureHit) => void;
|
|
64
|
+
export declare function actionToInteraction(action: string): AIInteractionType | undefined;
|
|
65
|
+
export declare function installAutocapture(sink: AutocaptureSink, options?: {
|
|
66
|
+
mask?: MaskMode;
|
|
67
|
+
}): () => void;
|
|
68
|
+
//# sourceMappingURL=autocapture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"autocapture.d.ts","sourceRoot":"","sources":["../src/autocapture.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEpF,eAAO,MAAM,IAAI;;;;;;;;;CASP,CAAC;AAEX,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,aAAa,CAAC;IACpB,SAAS,EAAE,oBAAoB,CAAC;IAChC,MAAM,EAAE,cAAc,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,eAAe,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,UAAU,GAAG,WAAW,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,qEAAqE;IACrE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,EAAE,WAAW,GAAG,WAAW,CAAC;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,WAAW,GAAG,SAAS,CAAC;AACtE,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;AAyC5D,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAiBjF;AAwDD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,EAAE,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,QAAQ,CAAA;CAAO,GAAG,MAAM,IAAI,CAqIvG"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broad, delegation-based interaction autocapture. data-abto-* annotations enrich
|
|
3
|
+
* the raw event; they never replace it with a guessed semantic event.
|
|
4
|
+
* SPA route changes emit pageview/pageleave. Session replay (rrweb) is out of scope.
|
|
5
|
+
*/
|
|
6
|
+
import { createClickSignalDetector } from './click-signals.js';
|
|
7
|
+
import { serializeElementsChain, elementText } from './elements-chain.js';
|
|
8
|
+
import { maskText, resolveDomPolicy } from './privacy.js';
|
|
9
|
+
import { createScrollDepthTracker, } from './scroll-depth.js';
|
|
10
|
+
export const ATTR = {
|
|
11
|
+
action: 'data-abto-action',
|
|
12
|
+
surface: 'data-abto-surface',
|
|
13
|
+
nodeId: 'data-abto-node-key',
|
|
14
|
+
requestId: 'data-abto-request-id',
|
|
15
|
+
conversationId: 'data-abto-conversation-id',
|
|
16
|
+
messageId: 'data-abto-message-id',
|
|
17
|
+
responseId: 'data-abto-response-id',
|
|
18
|
+
templateId: 'data-abto-template-id',
|
|
19
|
+
};
|
|
20
|
+
const HREF_CAP = 2048;
|
|
21
|
+
const NEVER_CAPTURE_INPUT_TYPES = new Set(['password', 'hidden']);
|
|
22
|
+
const SENSITIVE_FIELD_NAME = /(^|[_-])(cc|card|cardnum|credit|cvc|cvv|password|passwd|pwd|secret|ssn|social[_-]?security)([_-]|$)/i;
|
|
23
|
+
const SAFE_SEMANTIC_VALUE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
24
|
+
const IDENTIFIER_PATH_SEGMENT = /^(?:\d+|[0-9a-f]{8}-[0-9a-f-]{27,}|[A-Za-z0-9._~-]{20,}|[^/@\s]+@[^/@\s]+)$/i;
|
|
25
|
+
function safeSemanticValue(value) {
|
|
26
|
+
return value !== undefined && SAFE_SEMANTIC_VALUE.test(value) ? value : undefined;
|
|
27
|
+
}
|
|
28
|
+
function sanitizePath(pathname) {
|
|
29
|
+
const segments = pathname.split('/').map((segment) => {
|
|
30
|
+
if (segment === '')
|
|
31
|
+
return '';
|
|
32
|
+
let decoded = segment;
|
|
33
|
+
try {
|
|
34
|
+
decoded = decodeURIComponent(segment);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return ':id';
|
|
38
|
+
}
|
|
39
|
+
return IDENTIFIER_PATH_SEGMENT.test(decoded) ? ':id' : segment;
|
|
40
|
+
});
|
|
41
|
+
return segments.join('/') || '/';
|
|
42
|
+
}
|
|
43
|
+
function sanitizeUrl(raw) {
|
|
44
|
+
if (typeof location === 'undefined')
|
|
45
|
+
return undefined;
|
|
46
|
+
try {
|
|
47
|
+
const base = new URL(location.href);
|
|
48
|
+
const parsed = new URL(raw, base);
|
|
49
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
50
|
+
return undefined;
|
|
51
|
+
if (parsed.origin !== base.origin)
|
|
52
|
+
return parsed.origin.slice(0, HREF_CAP);
|
|
53
|
+
return sanitizePath(parsed.pathname).slice(0, HREF_CAP);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function actionToInteraction(action) {
|
|
60
|
+
switch (action) {
|
|
61
|
+
case 'copy':
|
|
62
|
+
case 'copied':
|
|
63
|
+
return 'copied';
|
|
64
|
+
case 'insert':
|
|
65
|
+
case 'inserted':
|
|
66
|
+
return 'inserted';
|
|
67
|
+
case 'accept':
|
|
68
|
+
case 'accepted':
|
|
69
|
+
return 'accepted';
|
|
70
|
+
case 'reject':
|
|
71
|
+
case 'rejected':
|
|
72
|
+
return 'rejected';
|
|
73
|
+
default:
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function readSemanticTarget(el) {
|
|
78
|
+
const inherited = (attr) => {
|
|
79
|
+
for (let node = el; node; node = node.parentElement) {
|
|
80
|
+
const value = node.getAttribute(attr);
|
|
81
|
+
if (value !== null)
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
};
|
|
86
|
+
const target = { action: safeSemanticValue(inherited(ATTR.action)) ?? '' };
|
|
87
|
+
const surface = safeSemanticValue(inherited(ATTR.surface));
|
|
88
|
+
const nodeId = safeSemanticValue(inherited(ATTR.nodeId));
|
|
89
|
+
const requestId = safeSemanticValue(inherited(ATTR.requestId));
|
|
90
|
+
const conversationId = safeSemanticValue(inherited(ATTR.conversationId));
|
|
91
|
+
const messageId = safeSemanticValue(inherited(ATTR.messageId));
|
|
92
|
+
const responseId = safeSemanticValue(inherited(ATTR.responseId));
|
|
93
|
+
const templateId = safeSemanticValue(inherited(ATTR.templateId));
|
|
94
|
+
if (surface !== undefined)
|
|
95
|
+
target.surface = surface;
|
|
96
|
+
if (nodeId !== undefined)
|
|
97
|
+
target.node_key = nodeId;
|
|
98
|
+
if (requestId !== undefined)
|
|
99
|
+
target.request_id = requestId;
|
|
100
|
+
if (conversationId !== undefined)
|
|
101
|
+
target.conversation_id = conversationId;
|
|
102
|
+
if (messageId !== undefined)
|
|
103
|
+
target.message_id = messageId;
|
|
104
|
+
if (responseId !== undefined)
|
|
105
|
+
target.response_id = responseId;
|
|
106
|
+
if (templateId !== undefined)
|
|
107
|
+
target.template_id = templateId;
|
|
108
|
+
return target;
|
|
109
|
+
}
|
|
110
|
+
function readElementMeta(el, eventType, mask) {
|
|
111
|
+
const meta = { tag: el.tagName.toLowerCase() };
|
|
112
|
+
const text = elementText(el);
|
|
113
|
+
if (text)
|
|
114
|
+
meta.text = maskText(text, mask);
|
|
115
|
+
const href = el.getAttribute('href');
|
|
116
|
+
const safeHref = href ? sanitizeUrl(href) : undefined;
|
|
117
|
+
if (safeHref)
|
|
118
|
+
meta.href = safeHref;
|
|
119
|
+
if (eventType === 'change') {
|
|
120
|
+
const field = el;
|
|
121
|
+
const inputType = (field.type ?? '').toLowerCase();
|
|
122
|
+
const name = el.getAttribute('name') ?? '';
|
|
123
|
+
const isProtectedField = NEVER_CAPTURE_INPUT_TYPES.has(inputType) || SENSITIVE_FIELD_NAME.test(name);
|
|
124
|
+
if (inputType)
|
|
125
|
+
meta.input_type = inputType;
|
|
126
|
+
const safeName = safeSemanticValue(name);
|
|
127
|
+
if (safeName)
|
|
128
|
+
meta.name = safeName;
|
|
129
|
+
if (!isProtectedField && typeof field.value === 'string' && field.value !== '') {
|
|
130
|
+
meta.value = maskText(field.value, mask);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return meta;
|
|
134
|
+
}
|
|
135
|
+
// capture phase: run before the host so a stopped propagation still reaches us.
|
|
136
|
+
const USE_CAPTURE = true;
|
|
137
|
+
export function installAutocapture(sink, options = {}) {
|
|
138
|
+
if (typeof document === 'undefined')
|
|
139
|
+
return () => { };
|
|
140
|
+
const maskMode = options.mask ?? 'all';
|
|
141
|
+
const emitInteraction = (el, eventType) => {
|
|
142
|
+
if (!el)
|
|
143
|
+
return;
|
|
144
|
+
const policy = resolveDomPolicy(el);
|
|
145
|
+
if (policy === 'no-capture')
|
|
146
|
+
return;
|
|
147
|
+
const mask = policy === 'include' ? 'off' : policy === 'sensitive' ? 'all' : maskMode;
|
|
148
|
+
const hit = {
|
|
149
|
+
kind: 'interaction',
|
|
150
|
+
eventType,
|
|
151
|
+
target: readSemanticTarget(el),
|
|
152
|
+
elementsChain: serializeElementsChain(el, mask),
|
|
153
|
+
element: readElementMeta(el, eventType, mask),
|
|
154
|
+
};
|
|
155
|
+
if (eventType === 'copy') {
|
|
156
|
+
hit.selectionLength = document.getSelection()?.toString().length ?? 0;
|
|
157
|
+
}
|
|
158
|
+
sink(hit);
|
|
159
|
+
};
|
|
160
|
+
const clickSignals = createClickSignalDetector(sink);
|
|
161
|
+
const onClick = (e) => {
|
|
162
|
+
const el = e.target;
|
|
163
|
+
emitInteraction(el, 'click');
|
|
164
|
+
if (el && resolveDomPolicy(el) !== 'no-capture' && e instanceof MouseEvent) {
|
|
165
|
+
const policy = resolveDomPolicy(el);
|
|
166
|
+
const mask = policy === 'include' ? 'off' : policy === 'sensitive' ? 'all' : maskMode;
|
|
167
|
+
clickSignals.onClick(el, e.clientX, e.clientY, mask);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const onChange = (e) => emitInteraction(e.target, 'change');
|
|
171
|
+
const onSubmit = (e) => emitInteraction(e.target, 'submit');
|
|
172
|
+
const onCopy = (e) => emitInteraction(e.target ?? document.activeElement, 'copy');
|
|
173
|
+
document.addEventListener('click', onClick, USE_CAPTURE);
|
|
174
|
+
document.addEventListener('change', onChange, USE_CAPTURE);
|
|
175
|
+
document.addEventListener('submit', onSubmit, USE_CAPTURE);
|
|
176
|
+
document.addEventListener('copy', onCopy, USE_CAPTURE);
|
|
177
|
+
const routePath = () => typeof location !== 'undefined' ? sanitizePath(location.pathname) : '';
|
|
178
|
+
let lastPath = routePath();
|
|
179
|
+
let pageviewStartedAt = Date.now();
|
|
180
|
+
const scrollDepth = createScrollDepthTracker();
|
|
181
|
+
const pageleaveHit = (unload) => {
|
|
182
|
+
const hit = {
|
|
183
|
+
kind: 'pageview',
|
|
184
|
+
eventType: 'pageleave',
|
|
185
|
+
path: lastPath,
|
|
186
|
+
durationMs: Date.now() - pageviewStartedAt,
|
|
187
|
+
};
|
|
188
|
+
const scroll = scrollDepth.snapshot();
|
|
189
|
+
if (scroll)
|
|
190
|
+
hit.scroll = scroll;
|
|
191
|
+
if (unload)
|
|
192
|
+
hit.unload = true;
|
|
193
|
+
return hit;
|
|
194
|
+
};
|
|
195
|
+
const emitRouteChange = () => {
|
|
196
|
+
const nextPath = routePath();
|
|
197
|
+
if (nextPath === lastPath)
|
|
198
|
+
return;
|
|
199
|
+
sink(pageleaveHit());
|
|
200
|
+
sink({ kind: 'pageview', eventType: 'pageview', path: nextPath, referrer: lastPath });
|
|
201
|
+
lastPath = nextPath;
|
|
202
|
+
pageviewStartedAt = Date.now();
|
|
203
|
+
scrollDepth.reset();
|
|
204
|
+
};
|
|
205
|
+
const history = typeof window !== 'undefined' ? window.history : undefined;
|
|
206
|
+
const originalPush = history?.pushState;
|
|
207
|
+
const originalReplace = history?.replaceState;
|
|
208
|
+
if (history && originalPush && originalReplace) {
|
|
209
|
+
history.pushState = function (...args) {
|
|
210
|
+
const result = originalPush.apply(this, args);
|
|
211
|
+
emitRouteChange();
|
|
212
|
+
return result;
|
|
213
|
+
};
|
|
214
|
+
history.replaceState = function (...args) {
|
|
215
|
+
const result = originalReplace.apply(this, args);
|
|
216
|
+
emitRouteChange();
|
|
217
|
+
return result;
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
// pageview 상태는 메모리에만 있으므로(PostHog PageViewManager 와 같은 모델),
|
|
221
|
+
// full unload 전에 마지막 pageleave 를 보내야 체류 시간이 유실되지 않는다.
|
|
222
|
+
let unloadPageleaveSent = false;
|
|
223
|
+
const onPagehide = () => {
|
|
224
|
+
if (unloadPageleaveSent)
|
|
225
|
+
return;
|
|
226
|
+
unloadPageleaveSent = true;
|
|
227
|
+
sink(pageleaveHit(true));
|
|
228
|
+
};
|
|
229
|
+
// bfcache 복원은 새 pageview 로 취급해 pageview 1개 ↔ pageleave 1개 짝을 유지한다.
|
|
230
|
+
const onPageshow = (e) => {
|
|
231
|
+
if (!e.persisted || !unloadPageleaveSent)
|
|
232
|
+
return;
|
|
233
|
+
unloadPageleaveSent = false;
|
|
234
|
+
pageviewStartedAt = Date.now();
|
|
235
|
+
scrollDepth.reset();
|
|
236
|
+
sink({ kind: 'pageview', eventType: 'pageview', path: lastPath });
|
|
237
|
+
};
|
|
238
|
+
if (typeof window !== 'undefined') {
|
|
239
|
+
window.addEventListener('popstate', emitRouteChange);
|
|
240
|
+
window.addEventListener('hashchange', emitRouteChange);
|
|
241
|
+
window.addEventListener('pagehide', onPagehide);
|
|
242
|
+
window.addEventListener('pageshow', onPageshow);
|
|
243
|
+
}
|
|
244
|
+
const initialPageview = { kind: 'pageview', eventType: 'pageview', path: lastPath };
|
|
245
|
+
const referrer = typeof document !== 'undefined' ? document.referrer : '';
|
|
246
|
+
const safeReferrer = referrer ? sanitizeUrl(referrer) : undefined;
|
|
247
|
+
if (safeReferrer)
|
|
248
|
+
initialPageview.referrer = safeReferrer;
|
|
249
|
+
sink(initialPageview);
|
|
250
|
+
return () => {
|
|
251
|
+
clickSignals.dispose();
|
|
252
|
+
scrollDepth.dispose();
|
|
253
|
+
document.removeEventListener('click', onClick, USE_CAPTURE);
|
|
254
|
+
document.removeEventListener('change', onChange, USE_CAPTURE);
|
|
255
|
+
document.removeEventListener('submit', onSubmit, USE_CAPTURE);
|
|
256
|
+
document.removeEventListener('copy', onCopy, USE_CAPTURE);
|
|
257
|
+
if (history && originalPush && originalReplace) {
|
|
258
|
+
history.pushState = originalPush;
|
|
259
|
+
history.replaceState = originalReplace;
|
|
260
|
+
}
|
|
261
|
+
if (typeof window !== 'undefined') {
|
|
262
|
+
window.removeEventListener('popstate', emitRouteChange);
|
|
263
|
+
window.removeEventListener('hashchange', emitRouteChange);
|
|
264
|
+
window.removeEventListener('pagehide', onPagehide);
|
|
265
|
+
window.removeEventListener('pageshow', onPageshow);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=autocapture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"autocapture.js","sourceRoot":"","sources":["../src/autocapture.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rageclick / deadclick 감지. click 위임 리스너에서 요소를 받아 UX 마찰 신호를 emit한다.
|
|
3
|
+
* rageclick: 짧은 시간·좁은 반경 내 반복 클릭. deadclick: 클릭 후 DOM 변화·스크롤이 없는 클릭.
|
|
4
|
+
*/
|
|
5
|
+
import type { SignalHit } from './autocapture.js';
|
|
6
|
+
import type { MaskMode } from './types.js';
|
|
7
|
+
export interface ClickSignalOptions {
|
|
8
|
+
rageClicks?: number;
|
|
9
|
+
rageWindowMs?: number;
|
|
10
|
+
rageRadiusPx?: number;
|
|
11
|
+
deadClickTimeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ClickSignalDetector {
|
|
14
|
+
onClick(el: Element, x: number, y: number, mask?: MaskMode): void;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
}
|
|
17
|
+
export declare function createClickSignalDetector(emit: (hit: SignalHit) => void, options?: ClickSignalOptions): ClickSignalDetector;
|
|
18
|
+
//# sourceMappingURL=click-signals.d.ts.map
|