@ait-co/devtools 0.1.19 → 0.1.20
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.en.md +863 -0
- package/README.md +104 -14
- package/dist/panel/index.js +125 -10
- package/dist/panel/index.js.map +1 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @ait-co/devtools
|
|
2
2
|
|
|
3
|
+
**한국어** · [English](./README.en.md)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@ait-co/devtools) [](./LICENSE)
|
|
6
|
+
|
|
3
7
|

|
|
4
8
|
|
|
5
9
|
`@apps-in-toss/web-framework` SDK의 mock 라이브러리입니다. `@apps-in-toss/web-bridge`, `@apps-in-toss/web-analytics` import도 함께 mock됩니다.
|
|
@@ -14,10 +18,6 @@
|
|
|
14
18
|
|
|
15
19
|
라이브 데모: <https://devtools.aitc.dev/> (이 repo의 `e2e/fixture/`를 GitHub Pages에 그대로 배포한 self-contained 데모).
|
|
16
20
|
|
|
17
|
-
## Reference consumer
|
|
18
|
-
|
|
19
|
-
[`sdk-example`](https://github.com/apps-in-toss-community/sdk-example)이 devtools의 reference consumer다. 모든 SDK API를 인터랙티브하게 실행해볼 수 있는 카탈로그 앱으로, 웹 데모는 <https://sdk-example.aitc.dev/>에서 바로 확인할 수 있다. 새 mock을 추가하면 sdk-example의 카드에서 그대로 동작하는 게 1차 sanity check. 단, 이 repo의 E2E suite는 sdk-example을 clone하지 않고 **내부 자기완결 fixture(`e2e/fixture/`)** 로 운영한다 — sdk-example이 깨져도 devtools CI는 영향받지 않는다.
|
|
20
|
-
|
|
21
21
|
## 설치
|
|
22
22
|
|
|
23
23
|
```bash
|
|
@@ -34,6 +34,10 @@ pnpm add -D @ait-co/devtools
|
|
|
34
34
|
> 안 되는" 상황을 방지하기 위한 의도적 동작입니다. 누락된 API는
|
|
35
35
|
> [이슈](https://github.com/apps-in-toss-community/devtools/issues)로 알려주세요.
|
|
36
36
|
|
|
37
|
+
## Reference consumer
|
|
38
|
+
|
|
39
|
+
[`sdk-example`](https://github.com/apps-in-toss-community/sdk-example)이 devtools의 reference consumer다. 모든 SDK API를 인터랙티브하게 실행해볼 수 있는 카탈로그 앱으로, 웹 데모는 <https://sdk-example.aitc.dev/>에서 바로 확인할 수 있다. 새 mock을 추가하면 sdk-example의 카드에서 그대로 동작하는 게 1차 sanity check. 단, 이 repo의 E2E suite는 sdk-example을 clone하지 않고 **내부 자기완결 fixture(`e2e/fixture/`)** 로 운영한다 — sdk-example이 깨져도 devtools CI는 영향받지 않는다.
|
|
40
|
+
|
|
37
41
|
## 번들러 설정
|
|
38
42
|
|
|
39
43
|
### Vite
|
|
@@ -215,15 +219,66 @@ if (process.env.NODE_ENV !== 'production') {
|
|
|
215
219
|
|
|
216
220
|
데스크톱 크롬에서 잘 돌던 미니앱을 **실제 폰**에서 보고 싶을 때. Vite dev 서버를 Cloudflare quick tunnel(`*.trycloudflare.com`, **계정 불필요**)로 노출하고, 폰에는 고정 URL의 launcher PWA를 한 번만 추가해 그 안에서 매번의 tunnel URL을 띄웁니다.
|
|
217
221
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
222
|
+
셋업은 세 갈래입니다:
|
|
223
|
+
|
|
224
|
+
- **프로젝트당 1회** — `vite.config`에 옵션 + `package.json`에 pnpm 설정 + (선택) `dev:phone` 스크립트
|
|
225
|
+
- **폰당 1회** — launcher PWA를 홈 화면에 추가
|
|
226
|
+
- **매 세션** — `pnpm dev:phone` (또는 `AIT_TUNNEL=1 pnpm dev`) 한 줄
|
|
227
|
+
|
|
228
|
+
### 1. 프로젝트당 1회 셋업
|
|
229
|
+
|
|
230
|
+
(a) **`vite.config.ts`에 `tunnel` 옵션 추가** — 항상 켜져 있어 매번 cloudflared가 떠도 괜찮으면 `tunnel: true`, 평소엔 끄고 명시할 때만 켜고 싶으면 env-gate 권장:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// vite.config.ts
|
|
234
|
+
import { defineConfig } from 'vite';
|
|
235
|
+
import aitDevtools from '@ait-co/devtools/unplugin';
|
|
236
|
+
|
|
237
|
+
export default defineConfig({
|
|
238
|
+
plugins: [
|
|
239
|
+
aitDevtools.vite({
|
|
240
|
+
tunnel: !!process.env.AIT_TUNNEL, // 평소 OFF, AIT_TUNNEL=1 일 때만 ON
|
|
241
|
+
}),
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
> `process.env.AIT_TUNNEL`은 `vite.config.ts`를 로드하는 시점(= vite 프로세스 기동 시)에 평가됩니다. 따라서 env 변수는 **vite를 띄우기 전에** 설정되어 있어야 합니다 (아래 (c)의 `dev:phone` 스크립트가 이를 자동으로 해결합니다).
|
|
247
|
+
|
|
248
|
+
(b) **`package.json`에 pnpm 10+ 빌드 스크립트 허용** — pnpm은 보안상 dependency의 postinstall을 기본 차단합니다. `cloudflared`는 postinstall에서 바이너리(~38 MB)를 받으므로 명시 허용 필요:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"pnpm": {
|
|
253
|
+
"onlyBuiltDependencies": ["cloudflared"]
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
> 명시하지 않아도 동작은 됩니다 — `tunnel.ts`가 첫 기동 시 `cloudflared.install()`을 lazy로 호출. 다만 `pnpm install`마다 "Ignored build scripts" 경고가 남고, 바이너리 다운로드가 첫 `pnpm dev` 시점으로 미뤄집니다. 참고: [`sdk-example#60`](https://github.com/apps-in-toss-community/sdk-example/pull/60).
|
|
259
|
+
|
|
260
|
+
(c) **(선택) `dev:phone` 스크립트** — env 변수 매번 타기 귀찮으면:
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"scripts": {
|
|
265
|
+
"dev": "vite",
|
|
266
|
+
"dev:phone": "AIT_TUNNEL=1 vite"
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### 2. 폰당 1회 셋업
|
|
272
|
+
|
|
273
|
+
폰에서 `https://devtools.aitc.dev/launcher/`를 열고 **홈 화면에 추가**합니다 (iOS Safari "공유 → 홈 화면에 추가", Android Chrome "앱 설치"). launcher 자체는 URL이 바뀌지 않으니 한 번만 하면 됩니다.
|
|
274
|
+
|
|
275
|
+
### 3. 매 세션
|
|
276
|
+
|
|
277
|
+
1. 데스크톱에서 `pnpm dev:phone`을 실행합니다 (1-(c) 스크립트를 추가하지 않았다면 `AIT_TUNNEL=1 pnpm dev`). 터미널에 `https://*.trycloudflare.com` URL + ASCII QR이 출력됩니다.
|
|
278
|
+
2. 폰의 launcher 아이콘 실행 → 카메라로 QR 스캔(또는 URL 붙여넣기) → 주소창 없는 풀스크린으로 dev 앱이 뜹니다.
|
|
279
|
+
3. 다음 세션엔 새 URL을 스캔만 하면 됩니다. launcher는 마지막 URL을 기억하고, "Rescan" 버튼으로 언제든 교체할 수 있습니다.
|
|
280
|
+
|
|
281
|
+
### 배경
|
|
227
282
|
|
|
228
283
|
> **왜 launcher를 거치나요?** quick tunnel URL은 매 실행마다 바뀌므로 그 URL 자체를 PWA로 설치하면 다음 세션엔 죽은 링크가 됩니다. cross-origin으로 페이지를 전환하면 iOS/Android 모두 standalone(크롬리스)이 깨집니다. → 고정 URL의 launcher를 한 번 설치하고, 그 안의 `<iframe>`으로 그날의 dev 앱을 full-bleed로 보여주는 구조입니다.
|
|
229
284
|
>
|
|
@@ -231,6 +286,10 @@ if (process.env.NODE_ENV !== 'production') {
|
|
|
231
286
|
>
|
|
232
287
|
> `tunnel` 옵션은 Vite dev 모드에서만 동작합니다 — production 빌드는 `forceEnable`이어도 터널을 띄우지 않습니다. 다른 번들러(Webpack/Rspack 등)에서는 무시됩니다. 이 옵션을 켜면 `cloudflared` / `qrcode-terminal`가 동적 import로만 로드되므로, 끄면 번들 그래프에 들어오지 않습니다.
|
|
233
288
|
|
|
289
|
+
### 한 줄 셋업 (예정)
|
|
290
|
+
|
|
291
|
+
위 "프로젝트당 1회" 단계(vite.config 패치 + `onlyBuiltDependencies` + `dev:phone` 스크립트)는 향후 [`agent-plugin`](https://github.com/apps-in-toss-community/agent-plugin)이 `/ait setup phone` 같은 단일 명령으로 흡수할 예정입니다 (명령 이름은 잠정). 이 README가 그 자동화의 명세서 역할을 하므로, 수동 셋업 단계가 줄어들어도 동작 모델 자체는 동일합니다.
|
|
292
|
+
|
|
234
293
|
## Device API 모드 시스템
|
|
235
294
|
|
|
236
295
|
디바이스 관련 API(카메라, 위치, 클립보드 등)는 세 가지 모드로 동작합니다:
|
|
@@ -384,7 +443,7 @@ Landscape로 전환하면:
|
|
|
384
443
|
**Show Apps in Toss nav bar** 토글(기본 on)을 켜면:
|
|
385
444
|
- 토스 호스트의 상단 nav bar(뒤로가기 / 앱 아이콘·이름 / ⋯ / ×)를 48px 높이로 오버레이
|
|
386
445
|
- status bar 바로 아래, safe area top 이후에 배치
|
|
387
|
-
- **중요**: 이 48px는 `env(safe-area-inset-top)` 및 `SafeAreaInsets.get().top`에 **포함되지 않습니다** (
|
|
446
|
+
- **중요**: 이 48px는 `env(safe-area-inset-top)` 및 `SafeAreaInsets.get().top`에 **포함되지 않습니다** (SDK 동작). 토스 측 예제들도 `insets.top + 48` 패턴으로 보정합니다.
|
|
388
447
|
|
|
389
448
|
### 콘솔에서 직접 조작
|
|
390
449
|
|
|
@@ -768,6 +827,37 @@ import '@ait-co/devtools/panel';
|
|
|
768
827
|
| `@ait-co/devtools/panel` | Floating DevTools Panel (import 시 자동 마운트) |
|
|
769
828
|
| `@ait-co/devtools/unplugin` | 번들러 플러그인 (.vite, .webpack, .rspack, .esbuild, .rollup) |
|
|
770
829
|
|
|
830
|
+
## 텔레메트리
|
|
831
|
+
|
|
832
|
+
devtools는 두 단계의 텔레메트리를 사용합니다.
|
|
833
|
+
|
|
834
|
+
### Tier 0 — 익명 사용 신호 (기본 ON, opt-out)
|
|
835
|
+
|
|
836
|
+
패널이 열릴 때 하루 1회 익명 ping을 전송합니다.
|
|
837
|
+
|
|
838
|
+
수집 항목: `source`, `version`, `ts` — PII 없음, `anon_id` 없음. 서버가 IP+UA 기반 daily hash를 생성하지만 저장하지 않습니다.
|
|
839
|
+
|
|
840
|
+
끄는 방법:
|
|
841
|
+
- 패널 Environment 탭 → "익명 사용 신호 (Tier 0)" 토글 OFF
|
|
842
|
+
- `localStorage.setItem('__ait_telemetry:t0_off', '1')` (콘솔에서 직접)
|
|
843
|
+
- 환경 변수: `AITC_TELEMETRY=off`
|
|
844
|
+
|
|
845
|
+
### Tier 1 — 확장 텔레메트리 (기본 OFF, opt-in)
|
|
846
|
+
|
|
847
|
+
패널 최초 실행 시 동의 토스트로 묻습니다. 동의한 경우에만 수집됩니다.
|
|
848
|
+
|
|
849
|
+
수집 항목: `panel_open`, `tab_view`, `session_duration` 이벤트 + 익명 UUID(`anon_id`).
|
|
850
|
+
|
|
851
|
+
끄는 방법:
|
|
852
|
+
- 패널 Environment 탭 → "확장 텔레메트리 (Tier 1)" 토글 OFF
|
|
853
|
+
- 수집된 데이터 삭제: 패널 Environment 탭 → "내 데이터 삭제"
|
|
854
|
+
|
|
855
|
+
개인정보 처리방침: <https://docs.aitc.dev/privacy>
|
|
856
|
+
|
|
771
857
|
## 라이센스
|
|
772
858
|
|
|
773
859
|
BSD 3-Clause
|
|
860
|
+
|
|
861
|
+
---
|
|
862
|
+
|
|
863
|
+
커뮤니티 오픈소스 프로젝트입니다.
|
package/dist/panel/index.js
CHANGED
|
@@ -36,7 +36,13 @@ const en = {
|
|
|
36
36
|
"env.row.safeArea.top": "Top",
|
|
37
37
|
"env.row.safeArea.bottom": "Bottom",
|
|
38
38
|
"env.telemetry.section": "Telemetry",
|
|
39
|
-
"env.telemetry.
|
|
39
|
+
"env.telemetry.t0Row": "Anonymous usage signal (Tier 0)",
|
|
40
|
+
"env.telemetry.t0On": "On",
|
|
41
|
+
"env.telemetry.t0Off": "Off",
|
|
42
|
+
"env.telemetry.t0TurnOn": "Turn on",
|
|
43
|
+
"env.telemetry.t0TurnOff": "Turn off",
|
|
44
|
+
"env.telemetry.t0Desc": "Version + date only, no PII. Once per day. Helps improve the package.",
|
|
45
|
+
"env.telemetry.row": "Extended telemetry (Tier 1)",
|
|
40
46
|
"env.telemetry.on": "On",
|
|
41
47
|
"env.telemetry.off": "Off",
|
|
42
48
|
"env.telemetry.turnOn": "Turn on",
|
|
@@ -49,7 +55,7 @@ const en = {
|
|
|
49
55
|
"env.telemetry.deleted": "Deleted",
|
|
50
56
|
"env.telemetry.deleteFailedRetry": "Delete failed (please retry)",
|
|
51
57
|
"env.telemetry.deleteFailed": "Delete failed",
|
|
52
|
-
"env.telemetry.privacyLink": "Privacy policy",
|
|
58
|
+
"env.telemetry.privacyLink": "Privacy policy →",
|
|
53
59
|
"env.section.language": "Language",
|
|
54
60
|
"env.language.row": "Language",
|
|
55
61
|
"env.language.ko": "한국어",
|
|
@@ -181,7 +187,13 @@ const ko = {
|
|
|
181
187
|
"env.row.safeArea.top": "Top",
|
|
182
188
|
"env.row.safeArea.bottom": "Bottom",
|
|
183
189
|
"env.telemetry.section": "Telemetry",
|
|
184
|
-
"env.telemetry.
|
|
190
|
+
"env.telemetry.t0Row": "익명 사용 신호 (Tier 0)",
|
|
191
|
+
"env.telemetry.t0On": "On",
|
|
192
|
+
"env.telemetry.t0Off": "Off",
|
|
193
|
+
"env.telemetry.t0TurnOn": "Turn on",
|
|
194
|
+
"env.telemetry.t0TurnOff": "Turn off",
|
|
195
|
+
"env.telemetry.t0Desc": "버전·날짜만 수집, PII 없음. 하루 1회. 패키지 개선에 사용됩니다.",
|
|
196
|
+
"env.telemetry.row": "확장 텔레메트리 (Tier 1)",
|
|
185
197
|
"env.telemetry.on": "On",
|
|
186
198
|
"env.telemetry.off": "Off",
|
|
187
199
|
"env.telemetry.turnOn": "Turn on",
|
|
@@ -194,7 +206,7 @@ const ko = {
|
|
|
194
206
|
"env.telemetry.deleted": "삭제 완료",
|
|
195
207
|
"env.telemetry.deleteFailedRetry": "삭제 실패 (다시 시도해주세요)",
|
|
196
208
|
"env.telemetry.deleteFailed": "삭제 실패",
|
|
197
|
-
"env.telemetry.privacyLink": "개인정보 처리방침",
|
|
209
|
+
"env.telemetry.privacyLink": "개인정보 처리방침 →",
|
|
198
210
|
"env.section.language": "Language",
|
|
199
211
|
"env.language.row": "Language",
|
|
200
212
|
"env.language.ko": "한국어",
|
|
@@ -720,11 +732,56 @@ const KEY_CONSENT = "__ait_telemetry:consent";
|
|
|
720
732
|
const KEY_REPROMPT_AFTER = "__ait_telemetry:reprompt_after";
|
|
721
733
|
const KEY_POLICY_VERSION = "__ait_telemetry:policy_version";
|
|
722
734
|
const KEY_ANON_ID = "__ait_telemetry:anon_id";
|
|
735
|
+
const KEY_T0_LAST_SENT = "__ait_telemetry:t0_last_sent";
|
|
736
|
+
const KEY_T0_OFF = "__ait_telemetry:t0_off";
|
|
737
|
+
/**
|
|
738
|
+
* Returns true if Tier 0 ping is enabled.
|
|
739
|
+
* Disabled when `localStorage.__ait_telemetry:t0_off = '1'`
|
|
740
|
+
* or `process.env.AITC_TELEMETRY === 'off'`.
|
|
741
|
+
*/
|
|
742
|
+
function isTier0Enabled() {
|
|
743
|
+
if (typeof process !== "undefined" && process.env.AITC_TELEMETRY === "off") return false;
|
|
744
|
+
try {
|
|
745
|
+
return localStorage.getItem(KEY_T0_OFF) !== "1";
|
|
746
|
+
} catch {
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Sets or clears the Tier 0 opt-out marker.
|
|
752
|
+
*/
|
|
753
|
+
function setTier0Enabled(enabled) {
|
|
754
|
+
try {
|
|
755
|
+
if (enabled) localStorage.removeItem(KEY_T0_OFF);
|
|
756
|
+
else localStorage.setItem(KEY_T0_OFF, "1");
|
|
757
|
+
} catch {}
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Returns true if Tier 0 has already been sent today (YYYY-MM-DD).
|
|
761
|
+
*/
|
|
762
|
+
function hasSentTier0Today() {
|
|
763
|
+
try {
|
|
764
|
+
const stored = localStorage.getItem(KEY_T0_LAST_SENT);
|
|
765
|
+
if (!stored) return false;
|
|
766
|
+
return stored === (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
767
|
+
} catch {
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Records that Tier 0 was sent today.
|
|
773
|
+
*/
|
|
774
|
+
function markTier0Sent() {
|
|
775
|
+
try {
|
|
776
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
777
|
+
localStorage.setItem(KEY_T0_LAST_SENT, today);
|
|
778
|
+
} catch {}
|
|
779
|
+
}
|
|
723
780
|
/**
|
|
724
781
|
* Current policy version. Bump this string whenever the privacy policy changes.
|
|
725
782
|
* Users who previously granted on an older version will be re-prompted once.
|
|
726
783
|
*/
|
|
727
|
-
const CURRENT_POLICY_VERSION = "2026-05-
|
|
784
|
+
const CURRENT_POLICY_VERSION = "2026-05-18";
|
|
728
785
|
/** 30 days in milliseconds */
|
|
729
786
|
const THIRTY_DAYS_MS = 720 * 60 * 60 * 1e3;
|
|
730
787
|
function readConsentState() {
|
|
@@ -763,7 +820,7 @@ function getOrCreateAnonId() {
|
|
|
763
820
|
function resolveEffectiveConsent() {
|
|
764
821
|
const raw = localStorage.getItem(KEY_CONSENT);
|
|
765
822
|
if (raw === "granted") {
|
|
766
|
-
if (readPolicyVersion() !== "2026-05-
|
|
823
|
+
if (readPolicyVersion() !== "2026-05-18") {
|
|
767
824
|
localStorage.removeItem(KEY_CONSENT);
|
|
768
825
|
localStorage.removeItem(KEY_POLICY_VERSION);
|
|
769
826
|
return "undecided";
|
|
@@ -889,6 +946,7 @@ function delay(ms) {
|
|
|
889
946
|
async function send(event, version, meta) {
|
|
890
947
|
if (readConsentState() !== "granted") return;
|
|
891
948
|
const payload = {
|
|
949
|
+
tier: 1,
|
|
892
950
|
source: "devtools",
|
|
893
951
|
event,
|
|
894
952
|
anon_id: getOrCreateAnonId(),
|
|
@@ -909,6 +967,7 @@ async function send(event, version, meta) {
|
|
|
909
967
|
function sendBeaconEvent(event, version, meta) {
|
|
910
968
|
if (readConsentState() !== "granted") return;
|
|
911
969
|
const payload = {
|
|
970
|
+
tier: 1,
|
|
912
971
|
source: "devtools",
|
|
913
972
|
event,
|
|
914
973
|
anon_id: getOrCreateAnonId(),
|
|
@@ -929,6 +988,49 @@ function sendBeaconEvent(event, version, meta) {
|
|
|
929
988
|
}).catch(() => {});
|
|
930
989
|
}
|
|
931
990
|
//#endregion
|
|
991
|
+
//#region src/telemetry/tier0.ts
|
|
992
|
+
/**
|
|
993
|
+
* Tier 0 telemetry — opt-out, fire-and-forget daily ping.
|
|
994
|
+
*
|
|
995
|
+
* Payload: { tier: 0, source: 'devtools', ts: number, version: string }
|
|
996
|
+
* No anon_id. No event name. No meta.
|
|
997
|
+
*
|
|
998
|
+
* Rules:
|
|
999
|
+
* - Sent once per calendar day (localStorage daily marker).
|
|
1000
|
+
* - Skipped when __ait_telemetry:t0_off = '1' or AITC_TELEMETRY=off.
|
|
1001
|
+
* - 5 s timeout, no retry. Failure is silently dropped.
|
|
1002
|
+
*/
|
|
1003
|
+
/**
|
|
1004
|
+
* Sends the Tier 0 daily ping if eligible.
|
|
1005
|
+
* Returns true if a ping was sent, false if skipped or failed.
|
|
1006
|
+
*/
|
|
1007
|
+
async function sendTier0Ping(version) {
|
|
1008
|
+
if (!isTier0Enabled()) return false;
|
|
1009
|
+
if (hasSentTier0Today()) return false;
|
|
1010
|
+
const payload = {
|
|
1011
|
+
tier: 0,
|
|
1012
|
+
source: "devtools",
|
|
1013
|
+
ts: Date.now(),
|
|
1014
|
+
version
|
|
1015
|
+
};
|
|
1016
|
+
const controller = new AbortController();
|
|
1017
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
1018
|
+
try {
|
|
1019
|
+
await fetch(`${TELEMETRY_ENDPOINT}/e`, {
|
|
1020
|
+
method: "POST",
|
|
1021
|
+
headers: { "Content-Type": "application/json" },
|
|
1022
|
+
body: JSON.stringify(payload),
|
|
1023
|
+
signal: controller.signal
|
|
1024
|
+
});
|
|
1025
|
+
markTier0Sent();
|
|
1026
|
+
return true;
|
|
1027
|
+
} catch {
|
|
1028
|
+
return false;
|
|
1029
|
+
} finally {
|
|
1030
|
+
clearTimeout(timeoutId);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
//#endregion
|
|
932
1034
|
//#region src/telemetry/index.ts
|
|
933
1035
|
/**
|
|
934
1036
|
* Telemetry client — internal to @ait-co/devtools.
|
|
@@ -948,7 +1050,7 @@ function readGlobalString(key) {
|
|
|
948
1050
|
}
|
|
949
1051
|
const TELEMETRY_ENDPOINT = readGlobalString("__TELEMETRY_ENDPOINT__") ?? "https://t.aitc.dev";
|
|
950
1052
|
function getVersion() {
|
|
951
|
-
return "0.1.
|
|
1053
|
+
return "0.1.20";
|
|
952
1054
|
}
|
|
953
1055
|
let panelVisibleSince = null;
|
|
954
1056
|
let accumulatedMs = 0;
|
|
@@ -972,11 +1074,12 @@ function wirePagehide() {
|
|
|
972
1074
|
}
|
|
973
1075
|
/**
|
|
974
1076
|
* Call once after panel mounts.
|
|
975
|
-
* Handles: consent check, optional toast, panel_mount event, pagehide wiring.
|
|
1077
|
+
* Handles: Tier 0 ping, consent check, optional toast, panel_mount event, pagehide wiring.
|
|
976
1078
|
*/
|
|
977
1079
|
function init() {
|
|
978
1080
|
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
979
1081
|
wirePagehide();
|
|
1082
|
+
sendTier0Ping(getVersion());
|
|
980
1083
|
if (resolveEffectiveConsent() === "granted") {
|
|
981
1084
|
getOrCreateAnonId();
|
|
982
1085
|
send("panel_mount", getVersion());
|
|
@@ -2450,6 +2553,18 @@ function buildLanguageSection() {
|
|
|
2450
2553
|
return h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.section.language")), h("div", { className: "ait-row" }, h("label", {}, t("env.language.row")), select));
|
|
2451
2554
|
}
|
|
2452
2555
|
function buildTelemetrySection() {
|
|
2556
|
+
const t0Enabled = isTier0Enabled();
|
|
2557
|
+
const t0StatusLabel = h("span", { style: `font-size:12px;font-weight:600;color:${t0Enabled ? "#4ade80" : "#888"}` }, t0Enabled ? t("env.telemetry.t0On") : t("env.telemetry.t0Off"));
|
|
2558
|
+
const t0ToggleBtn = h("button", {
|
|
2559
|
+
className: "ait-btn ait-btn-sm",
|
|
2560
|
+
style: "font-size:11px"
|
|
2561
|
+
}, t0Enabled ? t("env.telemetry.t0TurnOff") : t("env.telemetry.t0TurnOn"));
|
|
2562
|
+
t0ToggleBtn.addEventListener("click", () => {
|
|
2563
|
+
setTier0Enabled(!t0Enabled);
|
|
2564
|
+
window.dispatchEvent(new CustomEvent("__ait:panel-switch-tab", { detail: { tab: "env" } }));
|
|
2565
|
+
});
|
|
2566
|
+
const t0Row = h("div", { className: "ait-row" }, h("label", {}, t("env.telemetry.t0Row")), h("span", { style: "display:flex;align-items:center;gap:8px" }, t0StatusLabel, t0ToggleBtn));
|
|
2567
|
+
const t0Desc = h("div", { style: "font-size:11px;color:#666;margin-bottom:6px" }, t("env.telemetry.t0Desc"));
|
|
2453
2568
|
const isGranted = readConsentState() === "granted";
|
|
2454
2569
|
const statusLabel = h("span", { style: `font-size:12px;font-weight:600;color:${isGranted ? "#4ade80" : "#888"}` }, isGranted ? t("env.telemetry.on") : t("env.telemetry.off"));
|
|
2455
2570
|
const toggleBtn = h("button", {
|
|
@@ -2492,7 +2607,7 @@ function buildTelemetrySection() {
|
|
|
2492
2607
|
style: "font-size:11px;color:#666;text-decoration:none"
|
|
2493
2608
|
});
|
|
2494
2609
|
privacyLink.textContent = t("env.telemetry.privacyLink");
|
|
2495
|
-
return h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.telemetry.section")), statusRow, h("div", { style: "margin-bottom:6px" }, anonIdEl), h("div", {
|
|
2610
|
+
return h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("env.telemetry.section")), t0Row, t0Desc, statusRow, h("div", { style: "margin-bottom:6px" }, anonIdEl), h("div", {
|
|
2496
2611
|
className: "ait-btn-row",
|
|
2497
2612
|
style: "align-items:center;gap:8px;margin-top:6px"
|
|
2498
2613
|
}, deleteBtn, deleteStatus), h("div", { style: "margin-top:8px" }, privacyLink));
|
|
@@ -4052,7 +4167,7 @@ function mount() {
|
|
|
4052
4167
|
mockBadge.textContent = aitState.state.panelEditable ? t("panel.editMode.on") : t("panel.editMode.off");
|
|
4053
4168
|
refreshPanel();
|
|
4054
4169
|
});
|
|
4055
|
-
const headerRight = h("span", { style: "display:flex;align-items:center;gap:6px" }, mockBadge, h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v0.1.
|
|
4170
|
+
const headerRight = h("span", { style: "display:flex;align-items:center;gap:6px" }, mockBadge, h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v0.1.20`), closeBtn);
|
|
4056
4171
|
const header = h("div", { className: "ait-panel-header" }, h("span", {}, t("panel.title")), headerRight);
|
|
4057
4172
|
tabsEl = h("div", { className: "ait-panel-tabs" });
|
|
4058
4173
|
for (const tab of getTabs()) {
|