# keloola-errly-js

Universal JavaScript SDK untuk frontend error tracking. Menangkap JS errors,
unhandled promise rejections, dan HTTP errors di browser lalu mengirimnya ke
**Errly Dashboard**.

- 🪶 Ringan (~3 kB gzip), zero dependency
- 📦 ESM + CJS + UMD — bundler-friendly atau lewat `<script>`
- 🧩 Wrapper resmi: **React**, **Vue**, **Next.js**, **Nuxt**
- 🛡️ Tidak pernah meng-crash app host — semua operasi internal dibungkus `try/catch`
- 🔒 Strip data sensitif (password, token, kartu kredit) sebelum kirim

---

## Instalasi

```bash
npm install keloola-errly-js
```

Atau via CDN (vanilla JS):

```html
<script src="https://cdn.errly.app/keloola-errly-js.min.js"></script>
```

---

## Quick start

```js
import Errly from 'keloola-errly-js'

Errly.init({
  apiKey: 'proj_xxxxxxxxxxxx',          // dari dashboard Errly
  dsn: 'https://errly.yourdomain.com',  // URL dashboard
})
```

Setelah `init()`, global JS error & promise rejection otomatis tertangkap.

### Env variable per framework

SDK berjalan di browser, jadi API key harus di-expose ke client — perhatikan
prefix-nya:

| Framework / bundler | Akses env | Prefix wajib |
| --- | --- | --- |
| Vite (React/Vue + Vite) | `import.meta.env.VITE_ERRLY_API_KEY` | `VITE_` |
| Next.js | `process.env.NEXT_PUBLIC_ERRLY_API_KEY` | `NEXT_PUBLIC_` |
| Create React App | `process.env.REACT_APP_ERRLY_API_KEY` | `REACT_APP_` |
| Nuxt | `useRuntimeConfig().public.errlyApiKey` | via `runtimeConfig` |
| Astro | `import.meta.env.PUBLIC_ERRLY_API_KEY` | `PUBLIC_` |

---

## Konfigurasi (`Errly.init`)

```ts
Errly.init({
  apiKey: 'proj_xxxxxxxxxxxx',  // WAJIB
  dsn: 'https://...',           // WAJIB
  environment: 'production',    // default: window.location.hostname
  release: '1.0.0',             // versi app, opsional
  sampleRate: 1.0,              // 0.0–1.0, default 1.0 (100%)
  ignoreErrors: [],             // string | RegExp yang diabaikan
  beforeSend: null,             // hook modifikasi/cancel payload (return null = cancel)
  debug: false,                 // log ke console kalau true
  maxBreadcrumbs: 20,           // default 20
  attachStacktrace: true,       // default true
})
```

### Detail opsi

Ringkasan cepat:

| Opsi | Default | Singkatnya |
| --- | --- | --- |
| `sampleRate` | `1.0` | Berapa persen error dikirim (kontrol volume) |
| `ignoreErrors` | `[]` | Buang error yang message-nya cocok |
| `beforeSend` | `null` | Hook terakhir: modifikasi / batalkan payload |
| `debug` | `false` | Log aktivitas SDK ke console |
| `maxBreadcrumbs` | `20` | Jumlah jejak aksi user yang disimpan |
| `attachStacktrace` | `true` | Sertakan stack trace di payload |

Penjelasan lengkap di bawah.

---

#### `sampleRate` — default `1.0`

Membatasi **berapa banyak** error yang benar-benar dikirim. Berguna saat traffic
tinggi supaya dashboard tidak banjir dan hemat kuota. Nilainya `0.0`–`1.0`:

| Nilai | Efek |
| --- | --- |
| `1.0` | Kirim **semua** (100%) |
| `0.25` | Kirim **±25%**, sisanya dibuang acak |
| `0` | **Tidak** mengirim apa pun |

Di balik layar: `if (Math.random() > sampleRate) return`.

```js
Errly.init({ apiKey, dsn, sampleRate: 0.2 })  // kirim ~20% error
```

> ⚠️ **Dua hal penting:**
> 1. Sampling **per-event acak**, bukan per-sesi/per-user — satu user bisa saja
>    error pertamanya terkirim tapi error kedua tidak.
> 2. **Hanya berlaku di `capture()`** (exception: JS error global, promise
>    rejection, `Errly.capture()`). **`captureMessage()` tidak terpengaruh** —
>    jadi pesan manual *dan* auto-capture HTTP 5xx **selalu** terkirim 100%.

---

#### `ignoreErrors` — default `[]`

Daftar pola untuk **membuang** error yang message-nya cocok. Biasanya untuk noise
yang tidak bisa kamu kontrol: `ResizeObserver loop limit exceeded`, error dari
browser extension, `ChunkLoadError`, dll.

```js
Errly.init({
  apiKey, dsn,
  ignoreErrors: [
    'ResizeObserver loop',   // STRING = cocok kalau message MENGANDUNG teks ini
    /^Script error\.?$/,     // REGEXP = kontrol presisi
    /ChunkLoadError/,
  ],
})
```

- **String → substring** (`message.includes(pattern)`). `'Network'` akan
  mencocokkan `'NetworkError when fetching'`.
- **RegExp → `pattern.test(message)`**.

> ⚠️ Yang dicek **hanya `error.message`** — bukan stack trace, filename, atau URL.
> Berlaku di `capture()` **dan** `captureMessage()`.

---

#### `beforeSend` — default `null`

Hook **terakhir** sebelum payload dikirim. Titik kontrol paling fleksibel untuk
**memodifikasi**, **memperkaya**, atau **membatalkan** event.

```js
Errly.init({
  apiKey, dsn,
  beforeSend: (payload) => {
    // a) Batalkan kondisional → return null
    if (payload.message.includes('ResizeObserver')) return null

    // b) Tambah/ubah data
    payload.extra = { ...payload.extra, appVersion: '1.2.3' }

    // c) Scrub manual data sensitif yang lolos
    if (payload.user?.email) payload.user.email = '[hidden]'

    return payload   // WAJIB return payload (atau objek) kalau tidak mau batal
  },
})
```

- `return null` → event **dibuang**.
- `return payload` / objek → di-merge ke payload via `Object.assign`.

> ⚠️ Hook ini jalan **paling akhir** — setelah `sampleRate` & `ignoreErrors`
> (yang nge-gate lebih awal) dan setelah `stripSensitiveData()` otomatis. Jadi
> yang kamu terima sudah payload final; cocok untuk scrub/enrich tambahan.
> **Lupa `return`** (jadi `undefined`) → payload tetap terkirim apa adanya.

---

#### `debug` — default `false`

Kalau `true`, SDK menulis log ke console: saat init, tiap kirim
(`[Errly] Sending payload: …`), dan saat pengiriman gagal.

```js
Errly.init({ apiKey, dsn, debug: true })
// → [Errly] Initialized. Environment: localhost
// → [Errly] Sending payload: { type: 'exception', message: '…', … }
```

> ⚠️ **Matikan di production** supaya payload error tidak bocor ke console user.
> Semua file `test/*.html` memang pakai `debug: true`.

---

#### `maxBreadcrumbs` — default `20`

Batas jumlah **"jejak" aksi user** yang disimpan sebelum error (klik, navigasi,
request HTTP), lalu ikut dikirim di payload untuk membantu reproduksi.

Ini **ring buffer**: saat penuh, breadcrumb **paling lama dibuang**. Jadi
`maxBreadcrumbs: 20` = selalu simpan **20 jejak terbaru**.

```js
Errly.init({ apiKey, dsn, maxBreadcrumbs: 50 })  // konteks lebih panjang
```

> ⚠️ **Trade-off:** angka lebih besar = konteks lebih kaya, tapi **payload lebih
> berat** (tiap breadcrumb punya type, message, data, timestamp). 20 umumnya cukup.

---

#### `attachStacktrace` — default `true`

Sertakan **stack trace** di payload — info paling berharga untuk debugging
(file:line:col tempat error terjadi). Stack dipotong **maksimal 10 baris pertama**.

```js
Errly.init({ apiKey, dsn, attachStacktrace: false })  // tanpa stack (lebih ringan/privat)
```

> ⚠️ Hanya berpengaruh pada **exception via `capture()`**. `captureMessage()`
> memang tidak punya stack. `false` membuat debugging jauh lebih susah — umumnya
> biarkan `true`.

---

**Urutan pemrosesan satu error:**

```
Errly.capture(err)
  → sampleRate?         drop acak (capture() saja)
  → ignoreErrors?       drop kalau message cocok
  → buildPayload()      + browser, breadcrumbs (maxBreadcrumbs), stack (attachStacktrace)
  → stripSensitiveData  redact otomatis (password/token/kartu kredit)
  → beforeSend()        modifikasi / cancel
  → transport.send()    kirim (+ log kalau debug)
```

---

## Public API

| Method | Return | Singkatnya |
| --- | --- | --- |
| `init(options)` | `void` | Inisialisasi SDK + pasang semua handler |
| `capture(error, context?)` | `void` | Kirim exception manual |
| `captureMessage(msg, level?)` | `void` | Kirim pesan (bukan Error object) |
| `setUser(user)` | `void` | Set identitas user di tiap payload |
| `clearUser()` | `void` | Hapus identitas user |
| `addBreadcrumb(crumb)` | `void` | Tambah jejak aksi manual |
| `flush()` | `Promise<void>` | Tunggu semua event pending terkirim |

> 🛡️ **Semua method aman** — dibungkus `try/catch`, **tidak pernah throw** /
> meng-crash app. Kalau dipanggil **sebelum `init()`**, method diabaikan diam-diam
> (kecuali `flush()` yang mengembalikan `Promise.resolve()`).

---

#### `Errly.init(options)`

Inisialisasi SDK: validasi `apiKey`/`dsn`, lalu **pasang semua integrasi** —
`window.onerror`, `unhandledrejection`, fetch interceptor, dan breadcrumb tracker
(klik & navigasi). Wajib dipanggil **sekali** sebelum method lain.

```js
Errly.init({ apiKey: 'proj_xxx', dsn: 'https://errly.yourdomain.com' })
```

> ⚠️ Kalau `apiKey`/`dsn` kosong atau init gagal → muncul `console.warn` dan SDK
> jadi **no-op** (tidak crash). Lihat [Konfigurasi](#konfigurasi-errlyinit) untuk
> daftar opsi lengkap.

---

#### `Errly.capture(error, context?)`

Kirim **exception** secara manual (mis. di dalam `try/catch`). Argumen `error`
boleh `Error` atau nilai apa pun (otomatis di-coerce jadi `Error`). `context`
opsional:

```ts
Errly.capture(error, {
  level: 'error',                      // 'info' | 'warning' | 'error' | 'fatal' (default 'error')
  tags:  { section: 'checkout' },      // label pendek untuk filter di dashboard
  extra: { cartId, retryCount: 2 },    // data bebas untuk konteks
})
```

> ⚠️ **Kena `sampleRate` & `ignoreErrors`** (bisa di-drop). Stack ikut kalau
> `attachStacktrace: true`. Field `context.user` **tidak dipakai** — identitas
> user diambil dari `setUser()`, bukan dari sini.

---

#### `Errly.captureMessage(message, level?)`

Kirim **pesan** (bukan objek `Error`) — untuk log peristiwa penting yang bukan
exception. `level` default `'error'`.

```js
Errly.captureMessage('Pembayaran ditolak gateway', 'warning')
```

> ⚠️ `level` di sini hanya `'info' | 'warning' | 'error'`. Pesan **tidak punya
> stack trace**. Kena `ignoreErrors`, tapi **tidak kena `sampleRate`** (selalu
> terkirim).

---

#### `Errly.setUser(user)` / `Errly.clearUser()`

Set/hapus identitas user yang **otomatis disertakan di setiap payload**
berikutnya — mempermudah melacak error per-user di dashboard.

```js
// saat login
Errly.setUser({ id: 42, email: 'a@b.com', name: 'Andi', plan: 'pro' })  // boleh atribut custom

// saat logout
Errly.clearUser()
```

> 💡 `id`, `email`, `name` opsional, plus atribut custom bebas. Panggil
> `clearUser()` saat logout supaya error user berikutnya tidak salah atribusi.

---

#### `Errly.addBreadcrumb(crumb)`

Tambah **jejak** manual ke timeline (selain klik/navigasi/HTTP yang sudah
otomatis). Berguna menandai langkah bisnis penting sebelum error.

```js
Errly.addBreadcrumb({
  type: 'custom',                      // 'navigation' | 'click' | 'http' | 'console' | 'custom'
  category: 'checkout',
  message: 'User klik "Bayar sekarang"',
  data: { amount: 150000 },
})
```

> 💡 `timestamp` terisi otomatis kalau dikosongkan. Jejak disimpan sebanyak
> [`maxBreadcrumbs`](#maxbreadcrumbs--default-20) terbaru (ring buffer).

---

#### `Errly.flush()`

Mengembalikan `Promise` yang **resolve setelah semua event pending terkirim**.
Pengiriman SDK bersifat async/antrian, jadi panggil ini sebelum aksi yang bisa
menghentikan halaman (redirect, logout, `beforeunload`).

```js
await Errly.flush()
window.location.href = '/login'
```

---

## Framework wrappers

### React — `keloola-errly-js/react`

```tsx
import Errly from 'keloola-errly-js'
import { ErrlyErrorBoundary } from 'keloola-errly-js/react'

Errly.init({
  apiKey: import.meta.env.VITE_ERRLY_API_KEY,
  dsn: import.meta.env.VITE_ERRLY_DSN,
})

function App() {
  return (
    <ErrlyErrorBoundary fallback={<p>Oops, something broke.</p>}>
      <MyApp />
    </ErrlyErrorBoundary>
  )
}
```

Tersedia juga HOC `withErrly(Component, fallback?)`.

### Vue — `keloola-errly-js/vue`

```ts
import { createApp } from 'vue'
import { ErrlyVuePlugin } from 'keloola-errly-js/vue'
import App from './App.vue'

createApp(App)
  .use(ErrlyVuePlugin, {
    apiKey: import.meta.env.VITE_ERRLY_API_KEY,
    dsn: import.meta.env.VITE_ERRLY_DSN,
  })
  .mount('#app')
```

### Next.js (App Router, 14+) — `keloola-errly-js/next`

> ⚠️ **Jangan init lewat `instrumentation.ts`.** `register()` di sana hanya jalan
> di server (`NEXT_RUNTIME` = `'nodejs'`/`'edge'`, tak pernah `'browser'`),
> sedangkan SDK ini butuh `window`. Init dari **client component**.

**1.** Buat client component `app/errly-init.tsx`:

```tsx
'use client'
import { register } from 'keloola-errly-js/next'

register({
  apiKey: process.env.NEXT_PUBLIC_ERRLY_API_KEY!,
  dsn: process.env.NEXT_PUBLIC_ERRLY_DSN!,
  environment: process.env.NODE_ENV,
})

export function ErrlyInit() {
  return null
}
```

**2.** Pasang di `app/layout.tsx`:

```tsx
import { ErrlyInit } from './errly-init'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html><body><ErrlyInit />{children}</body></html>
  )
}
```

**3.** Tangkap render error di `app/global-error.tsx`:

```tsx
'use client'
import { useEffect } from 'react'
import Errly from 'keloola-errly-js'

export default function GlobalError({ error, reset }: { error: Error; reset: () => void }) {
  useEffect(() => { Errly.capture(error) }, [error])
  return (
    <html><body><h2>Something went wrong.</h2><button onClick={reset}>Try again</button></body></html>
  )
}
```

> ⚠️ Tanpa prefix `NEXT_PUBLIC_`, variable tidak tersedia di browser.
> 📌 Di **Next 15.3+**, isi `register(...)` boleh dipindah ke `instrumentation-client.ts`.

### Nuxt

Salin plugin berikut ke `plugins/errly.client.ts` (lihat `docs/integrations.md`):

```ts
export default defineNuxtPlugin(() => {
  const config = useRuntimeConfig()
  Errly.init({
    apiKey: config.public.errlyApiKey as string,
    dsn: config.public.errlyDsn as string,
  })
})
```

`nuxt.config.ts`:

```ts
export default defineNuxtConfig({
  plugins: ['~/plugins/errly.client.ts'],
  runtimeConfig: {
    public: {
      errlyApiKey: process.env.NUXT_PUBLIC_ERRLY_API_KEY,
      errlyDsn: process.env.NUXT_PUBLIC_ERRLY_DSN,
    },
  },
})
```

### Astro — `keloola-errly-js/astro`

Tambahkan integration di `astro.config.mjs` — script init otomatis ter-inject ke
setiap halaman, global error & promise rejection ditangkap handler bawaan SDK:

```js
import { defineConfig } from 'astro/config'
import { errly } from 'keloola-errly-js/astro'

export default defineConfig({
  integrations: [
    errly({
      apiKey: import.meta.env.PUBLIC_ERRLY_API_KEY,
      dsn: import.meta.env.PUBLIC_ERRLY_DSN,
    }),
  ],
})
```

> ⚠️ Astro pakai prefix `PUBLIC_` untuk env yang boleh diexpose ke client.

Untuk komponen island React/Vue di dalam Astro, pakai wrapper masing-masing
(`keloola-errly-js/react`, `/vue`) seperti biasa.

---

## Development

```bash
npm install          # install deps
npm test             # jalankan unit tests (vitest)
npm run test:coverage
npm run typecheck    # tsc --noEmit
npm run build        # build dist/ + wrapper (react/vue/next)
```

Build menghasilkan:

- `dist/keloola-errly.js` (ESM), `dist/keloola-errly.cjs` (CJS), `dist/keloola-errly.umd.js` (UMD)
- `dist/index.d.ts` (type definitions)
- `dist/{react,vue,next}/` (wrapper terkompilasi — ikut di-publish lewat `files: ["dist"]`)

Smoke-test manual di browser: `npx serve test/` lalu buka `test/manual.html`.

---

## Lisensi

MIT
