npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

iframe-tracking-sdk

v1.0.3

Published

SDK for secure and resilient Iframe tracking progress, offline queues, and automatic sync with backoff.

Readme

@bkt/iframe-tracking-sdk

@bkt/iframe-tracking-sdk là thư viện npm chuyên dụng giúp chuẩn hóa và tự động hóa toàn bộ quy trình thu thập sự kiện học tập (tracking) từ Iframe (Vite App / HTML) hoặc Mobile WebView (Flutter) lên Backend API.

Thư viện cung cấp hàng chờ bền vững bằng IndexedDB, bộ đồng bộ tự phục hồi Exponential Backoff, và class EventProcessor có thể tái sử dụng ở cả backend Node.js / NestJS.


🏗️ Kiến trúc Hệ thống

┌──────────────────────────┐     postMessage        ┌────────────────────────────────────────────────┐
│   SENDER (Iframe / App)  │  ──────────────────►   │           RECEIVER (Next.js Host)              │
│                          │   { type: LEARNING_    │                                                │
│  IframeClientTracker     │     EVENT, payload }   │  IframeHostTracker                             │
│  - emit(eventName, data) │                        │  - handleMessage() → validate origin           │
│  - setGameType()         │                        │  - enrich: user_id, app_id, event_id           │
│  - start() / stop()      │                        │  - enqueue() → EventQueueDB (IndexedDB)        │
└──────────────────────────┘                        └────────────────────┬───────────────────────────┘
                                                                         │
                                                                         ▼
                                                    ┌────────────────────────────────────────────────┐
                                                    │              SyncService                       │
                                                    │  - Batch mỗi 30s hoặc immediate sync          │
                                                    │    (unit_finish, unit_submit, unit_restart...) │
                                                    │  - Online/offline listener                     │
                                                    │  - Exponential Backoff (30s → 10m)             │
                                                    │  - 401 → refresh token → retry                 │
                                                    │  - 400/403/422 → Dead Letter Queue             │
                                                    └────────────────────┬───────────────────────────┘
                                                                         │  POST /api/progress/batch
                                                                         │  Bearer JWT
                                                                         ▼
                                                    ┌────────────────────────────────────────────────┐
                                                    │               Backend API                      │
                                                    │  Có thể dùng EventProcessor để phân loại,     │
                                                    │  normalize và build payload chuẩn             │
                                                    └────────────────────────────────────────────────┘

📂 Cấu trúc Module

| Module | Môi trường | Chức năng | |--------|-----------|-----------| | IframeClientTracker | Browser (Iframe) | Emit sự kiện qua postMessage hoặc Flutter bridge | | IframeHostTracker | Browser (Host) | Nhận, enrich và lưu sự kiện vào IndexedDB | | EventQueueDB | Browser | Hàng chờ bền vững IndexedDB với FIFO limit | | SyncService | Browser | Batch upload, retry backoff, dead letter | | useIframeHostTracking | React / Next.js | Hook tiện lợi bao bọc IframeHostTracker | | EventProcessor | Browser + Node.js | Pure logic: normalize, classify, buildBatch — dùng được ở backend | | security (utilities) | Browser + Node.js | HMAC-SHA256: generateNonce, signHmacSha256, verifyHmacSha256 |


⚡ Tính năng Cốt lõi

  • Hàng chờ offline IndexedDB (Durable Queue): Bảo toàn dữ liệu khi mất mạng, reload trang, đóng tab. FIFO limit maxQueueSize (mặc định 500), tự xóa 50 bản ghi cũ nhất khi đầy.
  • Đồng bộ thông minh: Gom lô 30s hoặc sync ngay (unit_finish, unit_submit, unit_restart, session_end, course_finish, pronunciation_score, client_error).
  • Exponential Backoff: Thử lại tăng dần 30s → 60s → 2m → 5m → 10m, tối đa 5 lần.
  • Dead Letter Queue: Lỗi 400/403/422 không thử lại, đẩy vào dead_letter. Tự dọn dẹp sau 7 ngày.
  • JWT Refresh Flow: Khi gặp 401, tự gọi onRefreshToken và retry 1 lần.
  • React Ready: Hook useIframeHostTracking đồng bộ hóa state UI đầy đủ.
  • Backend Compatible: EventProcessor không phụ thuộc browser API, dùng được trong NestJS/Node.js.

📦 Cài đặt

# Cài từ thư mục nội bộ
npm install ./iframe-tracking-sdk

# Hoặc từ registry (nếu đã publish)
npm install @bkt/iframe-tracking-sdk
# Biên dịch thư viện (TypeScript → JavaScript)
cd iframe-tracking-sdk
npm run build

🚀 Hướng dẫn Sử dụng

Luồng hoạt động: Iframe emit()postMessage → Host nhận → Lưu IndexedDB → SyncService gom batch → POST /api/progress/batch


Bước 1 — Phía Iframe (Vite App): Emit sự kiện

import { IframeClientTracker } from '@bkt/iframe-tracking-sdk';

const tracker = new IframeClientTracker({
  trustedHostOrigin: "https://host.bkt.edu.vn",
  gameType: "multiple_choice",
  debug: true,
});

tracker.start();

Emit theo luồng bài học:

// ① Vào bài — lưu queue, gửi batch 30s
await tracker.emit('unit_start', {
  unit_id: 'unit_lesson_01',
  course_id: 'course_eng_basic',
});

// ② Xem câu hỏi
await tracker.emit('question_view', {
  question_id: 'q_001',
  question_type: 'multiple_choice',
  unit_id: 'unit_lesson_01',
  text: 'This is an apple',
});

// ③ Trả lời câu hỏi
await tracker.emit('question_answer', {
  question_id: 'q_001',
  question_type: 'multiple_choice',
  is_correct: true,
  score: 10,
  duration_ms: 5200,
  text: 'This is an apple',
});

// ④ Nghe phát âm
await tracker.emit('audio_play', {
  audio_id: 'aud_apple_01',
  context: 'word_detail',
});

// ⑤ Kết quả phát âm AI — sync NGAY ⚡
await tracker.emit('pronunciation_score', {
  word_id: 'w_apple_001',
  word_text: 'Apple',
  overall_score: 85,
  accuracy_score: 82,
  fluency_score: 90,
});

// ⑥ Hoàn thành bài — sync NGAY ⚡
await tracker.emit('unit_finish', {
  unit_id: 'unit_lesson_01',
  course_id: 'course_eng_basic',
  score: 85,
  correct_answers: 17,
  total_questions: 20,
});

// ⑦ Nộp bài — sync NGAY ⚡
await tracker.emit('unit_submit', {
  unit_id: 'unit_lesson_01',
  attempts: 1,
});

Đổi loại game động:

tracker.setGameType('drag_the_words');

await tracker.emit('drag_drop_interaction', {
  exercise_id: 'ex_drag_001',
  item_id: 'word_subject',
  target_id: 'box_blank_01',
  is_correct: true,
  word_text: 'subject',
});

Thư viện tự phân luồng môi trường — cùng tracker.emit():

  • Web Iframewindow.parent.postMessage()
  • Flutter WebViewwindow.TrackingBridge.postMessage()
  • Standalone debugconsole.log()

Bước 2 — Phía Host (Next.js / React): Nhận, lưu và đồng bộ

"use client";
import React from 'react';
import { useIframeHostTracking } from '@bkt/iframe-tracking-sdk';

export default function LessonPage({ userId, lessonUrl }: {
  userId: string;
  lessonUrl: string;
}) {
  const {
    iframeRef,
    isReady,
    syncing,
    events,            // Danh sách event trong IndexedDB (debug)
    flush,             // Sync khẩn cấp toàn bộ queue ngay lập tức
    handleIframeLoad,  // Gắn vào onLoad của <iframe>
    clearLogs,
    clearQueue,
  } = useIframeHostTracking({
    // === BẮT BUỘC ===
    iframeUrl: lessonUrl,
    trustedOrigins: ["https://game.bkt.edu.vn"],
    userId: userId,
    appId: "bkt-kids-web",
    apiEndpoint: "/api/progress/batch",
    getJwtToken: async () => sessionStorage.getItem('access_token') || '',

    // === TÙY CHỌN ===
    sectionId: "section_001",  // Tự động chèn section_id vào mọi event
    batchIntervalMs: 30000,    // Chu kỳ batch (mặc định 30s)
    maxQueueSize: 500,
    maxRetryCount: 5,

    // Refresh token khi gặp lỗi 401
    onRefreshToken: async () => {
      const res = await fetch('/api/auth/refresh', { method: 'POST' });
      if (!res.ok) return false;
      const { token } = await res.json();
      sessionStorage.setItem('access_token', token);
      return token;
    },

    // Callback khi nhận event từ Iframe (trước khi lưu DB)
    onEventReceived: (event) => {
      console.log('[Tracking] Received:', event.event_name, event.payload);
    },

    // Callback khi sync lên server thành công
    onEventSynced: (eventIds) => {
      console.log('[Tracking] Synced:', eventIds.length, 'events');
    },

    // Callback khi cần reset UI (unit_restart / find_the_words)
    onSessionRefreshNeeded: (eventName) => {
      console.log('[Tracking] Session reset triggered by:', eventName);
    },

    // Callback khi event bị lỗi vĩnh viễn
    onDeadLetter: (event) => {
      console.error('[Tracking] Dead Letter:', event.event_name, event.error_message);
    },

    debug: process.env.NODE_ENV === 'development',
  });

  return (
    <div>
      {/* Nhúng Iframe — bắt buộc gắn ref và onLoad */}
      <iframe
        ref={iframeRef}
        src={lessonUrl}
        onLoad={handleIframeLoad}
        width={700}
        height={500}
        sandbox="allow-scripts allow-same-origin"
        title="Learning content"
      />

      {/* Thanh trạng thái (tùy chọn) */}
      <div style={{ fontSize: 12, color: '#888', marginTop: 8 }}>
        {syncing ? '🔄 Đang đồng bộ...' : isReady ? '✅ Sẵn sàng' : '⏳ Đang tải...'}
        <span style={{ marginLeft: 8 }}>Queue: {events.length} events</span>
        <button onClick={flush} disabled={syncing} style={{ marginLeft: 8 }}>
          Sync ngay
        </button>
      </div>
    </div>
  );
}

Không dùng React hook (vanilla JS / Vue):

import { IframeHostTracker } from '@bkt/iframe-tracking-sdk';

const tracker = new IframeHostTracker({
  iframeUrl: "https://game.bkt.edu.vn/embed/lesson-1",
  trustedOrigins: ["https://game.bkt.edu.vn"],
  userId: "u_opaque_bkt_99",
  appId: "bkt-kids-web",
  apiEndpoint: "/api/progress/batch",
  getJwtToken: () => sessionStorage.getItem('token') || '',
  debug: true,
});

tracker.start();

// Flush khi người dùng thoát trang
window.addEventListener('beforeunload', () => tracker.flush());

// Dừng khi unmount / rời trang
// tracker.stop();

Bước 3 — Phía Backend (NestJS / Node.js): Xử lý với EventProcessor

EventProcessor là class thuần, không dùng window hay IndexedDB — chạy hoàn toàn ở server:

import { EventProcessor, TrackingEvent } from '@bkt/iframe-tracking-sdk';

@Injectable()
export class ProgressBatchService {

  async handleBatch(body: { user_id: string; app_id: string; events: any[] }) {
    const { user_id, app_id, events: rawEvents } = body;

    // 1. Normalize & validate (lọc event thiếu event_name)
    const events = rawEvents
      .map(e => EventProcessor.normalize(e, { user_id, app_id }))
      .filter(Boolean) as TrackingEvent[];

    if (events.length === 0) return { accepted: 0 };

    // 2. Phân loại → route đến đúng service thống kê
    const groups = EventProcessor.groupByCategory(events);
    // groups.lifecycle  → unit_start, unit_finish, unit_submit, ...
    // groups.vocabulary → word_click, word_view, flashcard_flip, ...
    // groups.question   → question_answer, drag_drop_interaction, ...
    // groups.speaking   → pronunciation_score, pronunciation_record
    // groups.media      → audio_play, video_completed, ...

    await Promise.all([
      this.saveLifecycle(groups.lifecycle),
      this.saveVocabulary(groups.vocabulary),
      this.saveQuestion(groups.question),
      this.saveSpeaking(groups.speaking),
    ]);

    return { accepted: events.length };
  }

  private async saveSpeaking(events: TrackingEvent[]) {
    for (const event of events) {
      if (event.event_name !== 'pronunciation_score') continue;

      // extractScore tự xử lý: is_correct / result / overall_score >= 80
      const { isCorrect, score } = EventProcessor.extractScore(event);

      await this.db.upsertPronunciationStat({
        user_id: event.user_id,
        word_text: event.payload.word_text,
        overall_score: score,
        is_correct: isCorrect,
      });
    }
  }
}

Phân loại và kiểm tra đơn lẻ:

EventProcessor.classify('pronunciation_score'); // → 'speaking'
EventProcessor.classify('word_answer');          // → 'vocabulary'
EventProcessor.classify('video_completed');      // → 'media'

EventProcessor.isCritical('unit_finish');        // → true
EventProcessor.isCritical('word_click');         // → false

const { isCorrect, score } = EventProcessor.extractScore(event);
const payload = EventProcessor.parseIframeMessage(rawWsData); // WebSocket


## 🛠️ API Reference

### `IframeHostTrackerConfig`

| Tham số | Kiểu | Bắt buộc | Mô tả |
|---------|------|----------|-------|
| `iframeUrl` | `string` | ✅ | URL của iframe đang nhúng |
| `trustedOrigins` | `string[]` | ✅ | Whitelist origin được phép gửi event |
| `userId` | `string` | ✅ | Opaque User ID của học sinh |
| `appId` | `string` | ✅ | Mã ứng dụng (vd: `"bkt-kids-web-v1"`) |
| `apiEndpoint` | `string` | ✅ | Endpoint nhận batch: `/api/progress/batch` |
| `getJwtToken` | `() => Promise<string> \| string` | ✅ | Lấy Bearer JWT token để gửi API |
| `onRefreshToken` | `() => Promise<boolean \| string>` | ❌ | Gọi khi gặp 401, trả về `true` hoặc token mới |
| `batchIntervalMs` | `number` | ❌ | Chu kỳ batch (mặc định: `30000`ms) |
| `maxQueueSize` | `number` | ❌ | Giới hạn IndexedDB (mặc định: `500`) |
| `maxRetryCount` | `number` | ❌ | Số lần retry tối đa (mặc định: `5`) |
| `debug` | `boolean` | ❌ | Bật console logs chi tiết |
| `onEventReceived` | `(event: TrackingEvent) => void` | ❌ | Callback khi nhận event từ Iframe |
| `onEventSynced` | `(eventIds: string[]) => void` | ❌ | Callback khi sync thành công |
| `onDeadLetter` | `(event: TrackingEvent) => void` | ❌ | Callback khi event vào Dead Letter Queue |
| `onSyncStatusChange` | `(status: 'idle' \| 'syncing' \| 'error') => void` | ❌ | Callback thay đổi trạng thái sync |
| `onSessionRefreshNeeded` | `(eventName: string) => void` | ❌ | Callback khi nhận `unit_restart` hoặc `unit_start` (find_the_words) |

### `UseIframeHostTrackingResult` (React Hook Return)

| Property | Kiểu | Mô tả |
|----------|------|-------|
| `iframeRef` | `RefObject<HTMLIFrameElement>` | Gắn vào thẻ `<iframe>` |
| `isReady` | `boolean` | Tracker đã khởi động và sẵn sàng |
| `syncing` | `boolean` | Đang trong quá trình sync |
| `events` | `TrackingEvent[]` | Danh sách event từ IndexedDB (tối đa 100) |
| `flush` | `() => Promise<void>` | Sync khẩn cấp toàn bộ queue |
| `handleIframeLoad` | `() => void` | Gán vào `onLoad` của `<iframe>` |
| `clearLogs` | `() => void` | Xóa log hiển thị trên UI |
| `clearQueue` | `() => Promise<void>` | Xóa toàn bộ IndexedDB queue |
| `updateCredentials` | `(sessionId, hmacSecret) => void` | Cập nhật credentials local |
| `reinitiateHandshake` | `() => void` | Tái kết nối với iframe |

### `IframeClientTrackerConfig`

| Tham số | Kiểu | Mô tả |
|---------|------|-------|
| `trustedHostOrigin` | `string` | Origin của trang Host (Next.js) |
| `gameType` | `string` | Game type mặc định gắn vào mọi event |
| `debug` | `boolean` | Bật logs chi tiết trong iframe |
| `onHandshakeComplete` | `() => void` | Callback khi kênh sẵn sàng |

### `IframeClientTracker` Methods

| Method | Mô tả |
|--------|-------|
| `start()` | Bắt đầu lắng nghe, kích hoạt handshake |
| `stop()` | Dừng tracker, xóa listeners |
| `setGameType(type)` | Thay đổi game type động |
| `isReady()` | Kiểm tra kênh sẵn sàng |
| `emit(eventName, payload?, gameType?)` | Gửi sự kiện học tập lên Host |

### `EventProcessor` (Static Methods)

| Method | Môi trường | Mô tả |
|--------|-----------|-------|
| `normalize(raw, defaults?)` | Browser + Node.js | Validate & chuẩn hóa raw event → `TrackingEvent \| null` |
| `buildBatchPayload(events, appVersion?)` | Browser + Node.js | Build `BatchRequestBody` chuẩn API (không có `session_id`) |
| `classify(eventName)` | Browser + Node.js | Phân loại → `EventCategory` |
| `groupByCategory(events)` | Browser + Node.js | Nhóm events theo category |
| `isCritical(eventName)` | Browser + Node.js | Kiểm tra có cần immediate sync không |
| `extractScore(event)` | Browser + Node.js | Trích xuất `{ isCorrect, score }` từ payload |
| `parseIframeMessage(rawData)` | Browser + Node.js | Parse raw `postMessage` data → `LearningEventPayload \| null` |

**`EventCategory`** values: `'lifecycle'` | `'vocabulary'` | `'question'` | `'media'` | `'speaking'` | `'system'` | `'unknown'`

---

## 📊 Cấu trúc Payload Batch gửi lên API

Khi `SyncService` thực hiện đồng bộ, JSON gửi lên endpoint `POST /api/progress/batch`:

```json
{
  "user_id": "u_opaque_bkt_99",
  "app_id": "bkt-kids-web-v1",
  "app_version": "1.0.0",
  "events": [
    {
      "event_id": "evt_1716000000000_rj82kd9z",
      "event_name": "unit_finish",
      "timestamp": 1716000000000,
      "client_timestamp": 1716000000050,
      "unit_id": "unit_lesson_01",
      "course_id": "course_eng_basic",
      "score": 85,
      "correct_answers": 17,
      "total_questions": 20
    },
    {
      "event_id": "evt_1716000005000_ab12cd34",
      "event_name": "question_answer",
      "timestamp": 1716000005000,
      "client_timestamp": 1716000005020,
      "question_id": "q_001",
      "question_type": "multiple_choice",
      "is_correct": true,
      "score": 10,
      "duration_ms": 5200,
      "text": "This is an apple"
    }
  ]
}

Lưu ý quan trọng:

  • event_id theo format chuẩn: evt_[timestamp_ms]_[random_8_chars]
  • session_id không có trong payload API (chỉ dùng local debug)
  • Payload fields được flatten phẳng vào từng event object (không nested trong payload: {})
  • app_id chỉ 1 lần ở root body, không lặp lại trong từng event

🔄 Xử lý Ngoại lệ & Retry

POST /api/progress/batch thất bại
  ├── 200 OK (partial rejected_details)  → Delete accepted, Dead Letter rejected
  ├── 401 Unauthorized                   → Gọi onRefreshToken() → retry 1 lần
  │                                          Thất bại → pending, báo re-login
  ├── 400 Bad Request                    → ❌ Dead Letter ngay (lỗi cấu trúc)
  ├── 403 Forbidden                      → ❌ Dead Letter ngay (session hỏng)
  ├── 422 Unprocessable                  → ❌ Dead Letter ngay (HMAC sai)
  ├── 429 / 5xx Server Error             → 🔄 Exponential Backoff retry
  ├── Timeout (> 15s)                    → 🔄 Exponential Backoff retry
  └── Offline                            → Giữ queue, sync khi online trở lại

Exponential Backoff delays: 30s → 60s → 120s → 300s → 600s


📋 Sự kiện Immediate Sync (Không chờ batch 30s)

Các event sau đây kích hoạt upload ngay lập tức sau khi lưu vào IndexedDB:

| Event | Lý do | |-------|-------| | unit_finish | Hoàn thành bài học → ghi nhận điểm ngay | | unit_submit | Nộp bài → không được trễ | | unit_restart | Làm lại bài → backend cần tạo attempt mới | | session_end | Thoát phiên học | | course_finish | Hoàn thành khoá học | | pronunciation_score | Điểm phát âm AI | | client_error | Lỗi runtime cần alert ngay |


🔒 License

Thư viện được phân phối dưới giấy phép MIT.