npm package: @solapi/crm-browser
Description: SOLAPI CRM browser SDK for event tracking and record creation
Latest version: 0.1.4

# SOLAPI CRM Browser SDK

English | [한국어](#한국어)

A lightweight, production-ready JavaScript SDK for tracking user events and creating records in [SOLAPI](https://solapi.com) CRM from web browsers.

- **Homepage**: https://solapi.com
- **Tracking Key**: Issue at [SOLAPI Console > CRM > Settings > SDK](https://console.solapi.com/crm/settings/sdk)
- **API Documentation**: https://developers.solapi.com/references/crm-core

## Features

- **Event Tracking**: Track custom user events with flexible properties
- **Automatic Page View Tracking**: Monitor user navigation in single-page applications (SPAs)
- **Record Creation**: Create CRM records directly from your frontend
- **Session Identification**: Associate anonymous sessions with known records
- **Super Properties**: Register global properties sent with every event
- **Automatic Batching**: Queue and batch events for efficient network usage
- **Offline Resilience**: Queue events locally when network is unavailable
- **GDPR Compliant**: Full opt-in/opt-out tracking control
- **TypeScript Support**: Full type definitions included
- **Zero Dependencies**: Minimal bundle size (~2KB minified+gzipped)
- **Security**: HTTPS-only, prototype pollution prevention, cryptographic ID generation

## Installation

### npm

```bash
npm install @solapi/crm-browser
```

### Yarn

```bash
yarn add @solapi/crm-browser
```

### pnpm

```bash
pnpm add @solapi/crm-browser
```

### CDN (Script Tag)

```html
<script src="https://cdn.solapi.com/mfe/sdk/crm-sdk/solapi-crm.min.js"></script>
<script>
  SolapiCRM.init({ trackingKey: 'tk_live_...' });
</script>
```

## Quick Start

### 1. Initialize the SDK

Always call `init()` first with your tracking key:

```typescript
import SolapiCRM from '@solapi/crm-browser';

SolapiCRM.init({
  trackingKey: 'tk_live_your_tracking_key_here',
  entityId: 'entity_abc123',  // Optional: default entity for events
  autoPageView: true,          // Optional: auto-track page views
  debug: true,                 // Optional: enable debug logging
});
```

**Required**: `trackingKey` — [SOLAPI Console > CRM > Settings > SDK](https://console.solapi.com/crm/settings/sdk)에서 발급

### 2. Track Events

```typescript
// Simple event
SolapiCRM.track('signup');

// Event with properties
SolapiCRM.track('purchase', {
  product: 'Premium Plan',
  amount: 79000,
  currency: 'KRW',
});

// Event with analytics options
SolapiCRM.track('button_click', {}, {
  category: 'engagement',
  label: 'subscribe_button',
  value: 1,
});
```

### 3. Identify Users

Once a user logs in, associate the session with their CRM record:

```typescript
// After user login
const record = await SolapiCRM.createRecord({
  name: 'John Doe',
  data: { email: 'john@example.com' },
  tags: ['premium'],
});

// Associate session with the record
await SolapiCRM.identify(record.recordId, {
  email: 'john@example.com',
  plan: 'premium',
});
```

### 4. Register Super Properties

Send the same properties with every event:

```typescript
SolapiCRM.register({
  plan: 'premium',
  region: 'KR',
  appVersion: '1.2.0',
});
```

## Configuration

The `init()` method accepts a configuration object:

```typescript
interface SolapiCRMConfig {
  // Required
  trackingKey: string;

  // Optional
  entityId?: string;                // Default entity ID for events/records
  endpoint?: string;                // API endpoint (default: https://api.solapi.com/crm-core/v1/sdk)
  autoPageView?: boolean;           // Auto track page views on init (default: false)
  batchSize?: number;               // Events to queue before auto-flush (default: 10)
  flushInterval?: number;           // Auto-flush interval in milliseconds (default: 5000)
  maxQueueSize?: number;            // Max queued events before dropping oldest (default: 1000)
  debug?: boolean;                  // Enable debug logging (default: false)
}
```

### Configuration Examples

**Production Setup**:
```typescript
SolapiCRM.init({
  trackingKey: 'tk_live_xxxxxxxx',
  entityId: 'entity_main_app',
  batchSize: 20,        // Larger batches for efficiency
  flushInterval: 10000, // Less frequent flushes
});
```

**Development with Auto-Tracking**:
```typescript
SolapiCRM.init({
  trackingKey: 'tk_live_xxxxxxxx',
  autoPageView: true,
  debug: true,          // See all events logged
});
```

## API Reference

### init(config)

Initialize the SDK with a configuration object.

```typescript
SolapiCRM.init({
  trackingKey: 'tk_live_xxxxxxxx',
  entityId: 'entity_abc123',
  autoPageView: true,
});
```

**Throws**: `Error` if `trackingKey` is missing or invalid.

**Note**: Calling `init()` again will clean up and restart the SDK instance.

---

### track(eventType, properties?, options?)

Track a custom event.

```typescript
SolapiCRM.track('button_click', {
  buttonId: 'submit_btn',
  pageSection: 'checkout',
}, {
  category: 'engagement',
  label: 'checkout_submit',
  value: 1,
});
```

**Parameters**:
- `eventType` (string, required): Event name (e.g., `'purchase'`, `'login'`)
- `properties` (object, optional): Custom event data
- `options` (object, optional):
  - `category` (string): Event category for analytics
  - `label` (string): Event label
  - `value` (number): Numeric value for the event
  - `recordId` (string): Override the current record ID for this event only

**Throws**: `Error` if SDK not initialized or `eventType` is empty.

**Behavior**: If opt-out is active, event is silently discarded.

---

### trackPageView()

Manually track a page view.

```typescript
SolapiCRM.trackPageView();
```

**Behavior**:
- Captures current URL, path, page title, and referrer
- Sent as an event with `eventType: 'PAGE_VIEW'`
- Useful for manual tracking in non-SPA apps

**Throws**: `Error` if SDK not initialized.

---

### enableAutoPageView()

Install automatic SPA route change tracking.

```typescript
SolapiCRM.init({ trackingKey: 'tk_live_...' });
SolapiCRM.enableAutoPageView();
```

**Behavior**:
- Patches `history.pushState()` and `history.replaceState()`
- Listens for `popstate` events (browser back/forward)
- Tracks a new page view when routes change
- Safe to call multiple times (installs only once)

---

### createRecord(data)

Create a new CRM record.

```typescript
const record = await SolapiCRM.createRecord({
  entityId: 'entity_customers',
  name: 'John Doe',
  data: {
    email: 'john@example.com',
    phone: '010-1234-5678',
    source: 'web_signup',
  },
  tags: ['vip', 'early_adopter'],
});

console.log(record.recordId); // 'record_xyz789'
console.log(record.entityId); // 'entity_customers'
console.log(record.name);     // 'John Doe'
```

**Parameters**:
- `entityId` (string, optional): Entity type for the record (defaults to config entityId)
- `name` (string, required): Record name (e.g., user name, company name)
- `data` (object, optional): Custom record fields
- `tags` (string[], optional): Labels for organizing records

**Returns**: `Promise<CreateRecordResponse>`
```typescript
interface CreateRecordResponse {
  recordId: string;  // Unique record ID
  entityId: string;  // Entity type
  name: string;      // Record name
  [key: string]: any; // Additional response fields
}
```

**Throws**: `Error` if SDK not initialized or `name` is missing.

---

### identify(recordId, traits?)

Associate the current session with a known CRM record.

```typescript
// Basic identification
await SolapiCRM.identify('record_abc123');

// With trait updates
await SolapiCRM.identify('record_abc123', {
  email: 'user@example.com',
  plan: 'premium',
  lastLogin: new Date().toISOString(),
});
```

**Parameters**:
- `recordId` (string, required): The CRM record ID to identify as
- `traits` (object, optional): User properties to update on the record

**Behavior**:
- Merges all previous anonymous events to this record (server-side)
- Persists the record ID to localStorage for session continuity
- Network failures are logged but never throw (safe for host app)
- If switching from one known ID to another, sends identify request with traits

**Throws**: `Error` if SDK not initialized or `recordId` is empty.

**Best Practice**: Call this after user login or when record is known.

---

### register(properties)

Register super properties sent with every subsequent event.

```typescript
SolapiCRM.register({
  plan: 'premium',
  region: 'KR',
  userAgent: navigator.userAgent,
});

SolapiCRM.track('purchase', { amount: 79000 });
// Sent event includes: plan, region, userAgent, amount
```

**Parameters**:
- `properties` (object): Key-value pairs to attach to all future events

**Behavior**:
- Properties persist in memory only (cleared on page reload or `reset()`)
- Later calls overwrite earlier values for duplicate keys
- Prototype pollution safe (blocks `__proto__`, `constructor`, `prototype`)

---

### registerOnce(properties)

Register super properties, skipping keys that are already set.

```typescript
SolapiCRM.registerOnce({
  firstVisit: new Date().toISOString(),
  source: 'google_ads',
});

// Later calls don't overwrite these
SolapiCRM.registerOnce({
  firstVisit: '2099-01-01', // Skipped (already set)
  source: 'facebook_ads',   // Skipped (already set)
});
```

**Parameters**:
- `properties` (object): Key-value pairs to set if not already present

---

### unregister(propertyName)

Remove a super property.

```typescript
SolapiCRM.unregister('tempFlag');
```

**Parameters**:
- `propertyName` (string): The key to remove

---

### optOutTracking()

Opt out of all event tracking.

```typescript
SolapiCRM.optOutTracking();
// No events are sent until optInTracking() is called
```

**Behavior**:
- All `track()` calls silently return
- Event queue timer stops
- Persists in memory (not stored; resets on page reload)

**Use Case**: Respect user privacy preferences or GDPR consent.

---

### optInTracking()

Resume event tracking after opting out.

```typescript
SolapiCRM.optInTracking();
```

**Behavior**:
- Resumes accepting and sending events
- Restarts the event queue timer

---

### hasOptedOut()

Check the current opt-out status.

```typescript
if (SolapiCRM.hasOptedOut()) {
  console.log('Tracking is disabled');
}
```

**Returns**: `boolean` — `true` if opted out, `false` if tracking is active.

---

### reset()

Reset the user session identity.

```typescript
// On user logout
SolapiCRM.reset();
```

**Behavior**:
- Generates a new anonymous ID
- Clears all super properties
- Clears persisted record ID from localStorage
- Future events sent with the new anonymous ID

**Use Case**: Handle user logout or session reset.

---

### flush()

Immediately send all queued events to the server.

```typescript
await SolapiCRM.flush();
```

**Behavior**:
- Bypasses the automatic flush interval
- Retries up to 3 times with exponential backoff (1s, 2s, 4s)
- Batches events in groups of up to 100
- Dropped on persistent network failure after all retries

**Use Case**: Ensure events are sent before page unload (handled automatically).

---

### destroy()

Clean up and destroy the SDK instance.

```typescript
await SolapiCRM.destroy();
```

**Behavior**:
- Flushes all remaining queued events
- Restores original `history.pushState/replaceState` if patched
- Removes all event listeners
- Stops timers
- Clears internal state

**Use Case**: Cleanup in single-page app route changes or app unmount.

---

## Common Patterns

### React Integration

```typescript
// App.tsx
import { useEffect } from 'react';
import SolapiCRM from '@solapi/crm-browser';

export function App() {
  useEffect(() => {
    // Initialize on mount
    SolapiCRM.init({
      trackingKey: import.meta.env.VITE_TRACKING_KEY,
      entityId: 'entity_web_app',
      autoPageView: true,
    });

    // Cleanup on unmount
    return () => {
      SolapiCRM.destroy();
    };
  }, []);

  return <div>Your app</div>;
}
```

### User Authentication Flow

```typescript
// After successful login
const user = await loginUser(credentials);

const record = await SolapiCRM.createRecord({
  name: user.fullName,
  data: {
    email: user.email,
    role: user.role,
  },
});

await SolapiCRM.identify(record.recordId, {
  email: user.email,
  role: user.role,
});

// Now all events are tied to this user
SolapiCRM.track('login_success');
```

### SPA Route Navigation

```typescript
// In your router (e.g., React Router)
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import SolapiCRM from '@solapi/crm-browser';

export function RouteTracker() {
  const location = useLocation();

  useEffect(() => {
    // Manual page view on each route change
    SolapiCRM.trackPageView();
  }, [location]);

  return null;
}

// Or use enableAutoPageView() for automatic tracking
SolapiCRM.enableAutoPageView();
```

### GDPR Opt-Out Consent Banner

```typescript
function ConsentBanner() {
  const handleOptOut = () => {
    localStorage.setItem('tracking_consent', 'denied');
    SolapiCRM.optOutTracking();
  };

  const handleOptIn = () => {
    localStorage.setItem('tracking_consent', 'accepted');
    SolapiCRM.optInTracking();
  };

  return (
    <div className="consent-banner">
      <p>We use analytics to improve your experience.</p>
      <button onClick={handleOptOut}>Opt Out</button>
      <button onClick={handleOptIn}>Accept</button>
    </div>
  );
}

// On app load, respect stored consent
const consent = localStorage.getItem('tracking_consent');
if (consent === 'denied') {
  SolapiCRM.optOutTracking();
}
```

### Vue.js Integration

```typescript
// main.ts
import { createApp } from 'vue';
import SolapiCRM from '@solapi/crm-browser';
import App from './App.vue';

const app = createApp(App);

SolapiCRM.init({
  trackingKey: import.meta.env.VITE_TRACKING_KEY,
  entityId: 'entity_vue_app',
});

app.config.globalProperties.$crm = SolapiCRM;
app.mount('#app');
```

### Event Properties Best Practices

```typescript
// Good: Clear, lowercase keys, minimal nesting
SolapiCRM.track('add_to_cart', {
  product_id: '123',
  product_name: 'Laptop',
  price: 1500000,
  currency: 'KRW',
  quantity: 1,
});

// Avoid: Nested objects, inconsistent naming, excessive data
SolapiCRM.track('add_to_cart', {
  product: {
    id: '123',
    name: 'Laptop',
    pricing: {
      amount: 1500000,
    },
  },
  ProductQuantity: 1,  // Inconsistent casing
});
```

## Browser Compatibility

The SDK requires modern browser APIs. Minimum support:

| Browser | Minimum Version |
|---------|-----------------|
| Chrome  | 57+             |
| Firefox | 52+             |
| Safari  | 11+             |
| Edge    | 79+             |
| iOS Safari | 11+         |
| Android Chrome | 57+     |

**Required APIs**:
- `fetch()`
- `crypto.getRandomValues()`
- `localStorage` (optional; falls back gracefully)
- `history.pushState/replaceState` (for SPA tracking)

**Graceful Fallbacks**:
- If `localStorage` is unavailable (private mode, iframe sandbox), IDs are generated per-session
- If historical API patching fails, manual `trackPageView()` calls still work
- Network failures are caught and logged; never crash the host app

## TypeScript Support

Full TypeScript support with exported types:

```typescript
import type { SolapiCRMConfig, TrackEvent } from '@solapi/crm-browser';
import SolapiCRM from '@solapi/crm-browser';

const config: SolapiCRMConfig = {
  trackingKey: 'tk_live_...',
  entityId: 'entity_main',
  debug: true,
};

SolapiCRM.init(config);

interface CustomEvent extends TrackEvent {
  customProperty: string;
}
```

## Debug Mode

Enable detailed logging to troubleshoot issues:

```typescript
SolapiCRM.init({
  trackingKey: 'tk_live_...',
  debug: true,
});

// Console output:
// [SolapiCRM] Event queued: purchase
// [SolapiCRM] Flushing 5 events...
// [SolapiCRM] Events sent successfully
```

## Security Considerations

- **HTTPS Only**: SDK enforces HTTPS transport (localhost exempted for development)
- **Tracking Key Security**: Never expose `trackingKey` on public repositories. Use environment variables.
- **Prototype Pollution**: Property keys like `__proto__`, `constructor`, `prototype` are automatically blocked
- **ID Generation**: Uses `crypto.getRandomValues()` with rejection sampling for cryptographically secure IDs
- **No Sensitive Data**: Avoid sending passwords, credit card numbers, or PII in event properties

**Best Practice**:
```typescript
// Store tracking key in environment variables
const config = {
  trackingKey: process.env.REACT_APP_TRACKING_KEY,
};

// Don't do this:
const config = {
  trackingKey: 'tk_live_hardcoded_key', // Visible in source code
};
```

## Performance Tips

1. **Batch Events**: Queue events and flush periodically instead of individual requests
2. **Limit Super Properties**: Register only essential global properties
3. **Use Appropriate Batch Size**: Balance between network overhead and queue memory

```typescript
SolapiCRM.init({
  trackingKey: 'tk_live_...',
  batchSize: 20,        // Flush after 20 events (default: 10)
  flushInterval: 10000, // Flush every 10 seconds (default: 5s)
});
```

4. **Clean Up**: Call `destroy()` when SDK is no longer needed

## Storage

The SDK uses browser localStorage to persist:

| Key | Value | Purpose |
|-----|-------|---------|
| `_solapi_anon_id` | Randomly generated ID | Persistent anonymous user ID |
| `_solapi_record_id` | Record ID from `identify()` | Persistent identified user |

**Note**: Storage is not used if localStorage is unavailable (e.g., private mode, iframe with sandbox).

## Error Handling

The SDK is designed to never break the host application:

```typescript
try {
  // Always safe — errors are caught and logged
  SolapiCRM.track('event_name', { customData: true });
} catch (error) {
  // This rarely happens (only on init)
  console.error('SolapiCRM error:', error);
}
```

**Network Failures**: Events are retried up to 3 times with exponential backoff. On persistent failure, events are dropped and logged (if debug is enabled).

**Initialization**: Only `init()` throws errors if required config is missing.

## Changelog

### 0.1.0 (Initial Release)
- Event tracking with custom properties
- Page view tracking for SPAs
- Record creation and identification
- Super properties support
- Opt-in/opt-out GDPR compliance
- Automatic batching and retry logic
- TypeScript support
- Browser SDK distribution (IIFE + ESM)

## License

MIT

---

---

## 한국어

# SOLAPI CRM 브라우저 SDK

웹 브라우저에서 [SOLAPI](https://solapi.com) CRM의 사용자 이벤트를 추적하고 레코드를 생성하기 위한 가볍고 본격적인 JavaScript SDK입니다.

- **홈페이지**: https://solapi.com
- **트래킹 키 발급**: [SOLAPI 콘솔 > CRM > 설정 > SDK](https://console.solapi.com/crm/settings/sdk)
- **API 문서**: https://developers.solapi.com/references/crm-core

## 주요 기능

- **이벤트 추적**: 유연한 속성을 가진 사용자 정의 이벤트 추적
- **자동 페이지 뷰 추적**: 단일 페이지 애플리케이션(SPA)의 사용자 네비게이션 모니터링
- **레코드 생성**: 프론트엔드에서 직접 CRM 레코드 생성
- **세션 식별**: 익명 세션을 알려진 레코드와 연결
- **슈퍼 속성**: 모든 이벤트와 함께 전송되는 전역 속성 등록
- **자동 배칭**: 효율적인 네트워크 사용을 위한 이벤트 큐잉 및 배칭
- **오프라인 복원력**: 네트워크를 사용할 수 없을 때 로컬에서 이벤트 대기
- **GDPR 준수**: 추적의 전체 옵트인/옵트아웃 제어
- **TypeScript 지원**: 완전한 타입 정의 포함
- **의존성 없음**: 최소 번들 크기 (~2KB minified+gzipped)
- **보안**: HTTPS만 지원, 프로토타입 오염 방지, 암호학적 ID 생성

## 설치

### npm

```bash
npm install @solapi/crm-browser
```

### Yarn

```bash
yarn add @solapi/crm-browser
```

### pnpm

```bash
pnpm add @solapi/crm-browser
```

### CDN (Script 태그)

```html
<script src="https://cdn.solapi.com/mfe/sdk/crm-sdk/solapi-crm.min.js"></script>
<script>
  SolapiCRM.init({ trackingKey: 'tk_live_...' });
</script>
```

## 빠른 시작

### 1. SDK 초기화

항상 추적 키를 사용하여 먼저 `init()`을 호출합니다:

```typescript
import SolapiCRM from '@solapi/crm-browser';

SolapiCRM.init({
  trackingKey: 'tk_live_your_tracking_key_here',
  entityId: 'entity_abc123',  // 선택사항: 이벤트 기본 개체
  autoPageView: true,          // 선택사항: 페이지 뷰 자동 추적
  debug: true,                 // 선택사항: 디버그 로깅 활성화
});
```

**필수**: `trackingKey` — [SOLAPI 콘솔 > CRM > 설정 > SDK](https://console.solapi.com/crm/settings/sdk)에서 발급

### 2. 이벤트 추적

```typescript
// 간단한 이벤트
SolapiCRM.track('signup');

// 속성이 있는 이벤트
SolapiCRM.track('purchase', {
  product: 'Premium Plan',
  amount: 79000,
  currency: 'KRW',
});

// 분석 옵션이 있는 이벤트
SolapiCRM.track('button_click', {}, {
  category: 'engagement',
  label: 'subscribe_button',
  value: 1,
});
```

### 3. 사용자 식별

사용자 로그인 후 세션을 CRM 레코드와 연결합니다:

```typescript
// 사용자 로그인 후
const record = await SolapiCRM.createRecord({
  name: 'John Doe',
  data: { email: 'john@example.com' },
  tags: ['premium'],
});

// 세션을 레코드와 연결
await SolapiCRM.identify(record.recordId, {
  email: 'john@example.com',
  plan: 'premium',
});
```

### 4. 슈퍼 속성 등록

모든 이벤트와 함께 동일한 속성을 전송합니다:

```typescript
SolapiCRM.register({
  plan: 'premium',
  region: 'KR',
  appVersion: '1.2.0',
});
```

## 설정

`init()` 메서드는 설정 객체를 허용합니다:

```typescript
interface SolapiCRMConfig {
  // 필수
  trackingKey: string;

  // 선택사항
  entityId?: string;                // 이벤트/레코드의 기본 개체 ID
  endpoint?: string;                // API 엔드포인트 (기본값: https://api.solapi.com/crm-core/v1/sdk)
  autoPageView?: boolean;           // init 시 자동으로 페이지 뷰 추적 (기본값: false)
  batchSize?: number;               // 자동 플러시 전 큐잉할 이벤트 수 (기본값: 10)
  flushInterval?: number;           // 자동 플러시 간격(밀리초) (기본값: 5000)
  maxQueueSize?: number;            // 최대 큐 크기, 초과 시 가장 오래된 항목 삭제 (기본값: 1000)
  debug?: boolean;                  // 디버그 로깅 활성화 (기본값: false)
}
```

### 설정 예제

**프로덕션 설정**:
```typescript
SolapiCRM.init({
  trackingKey: 'tk_live_xxxxxxxx',
  entityId: 'entity_main_app',
  batchSize: 20,        // 효율성을 위한 더 큰 배치
  flushInterval: 10000, // 덜 빈번한 플러시
});
```

**자동 추적을 사용한 개발**:
```typescript
SolapiCRM.init({
  trackingKey: 'tk_live_xxxxxxxx',
  autoPageView: true,
  debug: true,          // 모든 이벤트 로깅 확인
});
```

## API 참조

### init(config)

설정 객체로 SDK를 초기화합니다.

```typescript
SolapiCRM.init({
  trackingKey: 'tk_live_xxxxxxxx',
  entityId: 'entity_abc123',
  autoPageView: true,
});
```

**예외**: `trackingKey`가 누락되거나 유효하지 않으면 `Error` 발생.

**참고**: `init()`을 다시 호출하면 이전 인스턴스를 정리하고 다시 시작합니다.

---

### track(eventType, properties?, options?)

사용자 정의 이벤트를 추적합니다.

```typescript
SolapiCRM.track('button_click', {
  buttonId: 'submit_btn',
  pageSection: 'checkout',
}, {
  category: 'engagement',
  label: 'checkout_submit',
  value: 1,
});
```

**매개변수**:
- `eventType` (문자열, 필수): 이벤트 이름 (예: `'purchase'`, `'login'`)
- `properties` (객체, 선택): 사용자 정의 이벤트 데이터
- `options` (객체, 선택):
  - `category` (문자열): 분석을 위한 이벤트 카테고리
  - `label` (문자열): 이벤트 레이블
  - `value` (숫자): 이벤트의 수치 값
  - `recordId` (문자열): 이 이벤트만 현재 레코드 ID 무시

**예외**: SDK가 초기화되지 않았거나 `eventType`이 비어있으면 `Error` 발생.

**동작**: 옵트아웃이 활성화되면 이벤트가 자동으로 삭제됩니다.

---

### trackPageView()

페이지 뷰를 수동으로 추적합니다.

```typescript
SolapiCRM.trackPageView();
```

**동작**:
- 현재 URL, 경로, 페이지 제목 및 리퍼러를 캡처합니다
- `eventType: 'PAGE_VIEW'`인 이벤트로 전송됩니다
- 비 SPA 앱의 수동 추적에 유용합니다

**예외**: SDK가 초기화되지 않으면 `Error` 발생.

---

### enableAutoPageView()

자동 SPA 라우트 변경 추적을 설치합니다.

```typescript
SolapiCRM.init({ trackingKey: 'tk_live_...' });
SolapiCRM.enableAutoPageView();
```

**동작**:
- `history.pushState()`와 `history.replaceState()` 패치
- `popstate` 이벤트 리스너 (브라우저 뒤로/앞으로)
- 라우트 변경 시 새로운 페이지 뷰 추적
- 여러 번 호출해도 안전 (한 번만 설치)

---

### createRecord(data)

새로운 CRM 레코드를 생성합니다.

```typescript
const record = await SolapiCRM.createRecord({
  entityId: 'entity_customers',
  name: 'John Doe',
  data: {
    email: 'john@example.com',
    phone: '010-1234-5678',
    source: 'web_signup',
  },
  tags: ['vip', 'early_adopter'],
});

console.log(record.recordId); // 'record_xyz789'
console.log(record.entityId); // 'entity_customers'
console.log(record.name);     // 'John Doe'
```

**매개변수**:
- `entityId` (문자열, 선택): 레코드의 개체 유형 (설정 entityId 기본값)
- `name` (문자열, 필수): 레코드 이름 (예: 사용자 이름, 회사명)
- `data` (객체, 선택): 사용자 정의 레코드 필드
- `tags` (문자열[], 선택): 레코드 구성을 위한 레이블

**반환**: `Promise<CreateRecordResponse>`
```typescript
interface CreateRecordResponse {
  recordId: string;  // 고유 레코드 ID
  entityId: string;  // 개체 유형
  name: string;      // 레코드 이름
  [key: string]: any; // 추가 응답 필드
}
```

**예외**: SDK가 초기화되지 않았거나 `name`이 누락되면 `Error` 발생.

---

### identify(recordId, traits?)

현재 세션을 알려진 CRM 레코드와 연결합니다.

```typescript
// 기본 식별
await SolapiCRM.identify('record_abc123');

// 특성 업데이트 포함
await SolapiCRM.identify('record_abc123', {
  email: 'user@example.com',
  plan: 'premium',
  lastLogin: new Date().toISOString(),
});
```

**매개변수**:
- `recordId` (문자열, 필수): 식별할 CRM 레코드 ID
- `traits` (객체, 선택): 레코드에 업데이트할 사용자 속성

**동작**:
- 모든 이전 익명 이벤트를 이 레코드로 병합 (서버 쪽)
- localStorage에 레코드 ID를 저장하여 세션 연속성 유지
- 네트워크 실패는 로깅되지만 절대 발생하지 않음 (호스트 앱 안전)
- 알려진 ID에서 다른 ID로 전환 시 특성과 함께 식별 요청 전송

**예외**: SDK가 초기화되지 않았거나 `recordId`가 비어있으면 `Error` 발생.

**모범 사례**: 사용자 로그인 후 또는 레코드가 알려져 있을 때 호출합니다.

---

### register(properties)

모든 후속 이벤트와 함께 전송되는 슈퍼 속성을 등록합니다.

```typescript
SolapiCRM.register({
  plan: 'premium',
  region: 'KR',
  userAgent: navigator.userAgent,
});

SolapiCRM.track('purchase', { amount: 79000 });
// 전송된 이벤트는 다음을 포함: plan, region, userAgent, amount
```

**매개변수**:
- `properties` (객체): 모든 향후 이벤트에 첨부할 키-값 쌍

**동작**:
- 속성은 메모리에만 저장됨 (페이지 새로고침 또는 `reset()` 시 삭제)
- 이후 호출이 중복 키에 대한 이전 값을 덮어씁니다
- 프로토타입 오염 안전 (`__proto__`, `constructor`, `prototype` 차단)

---

### registerOnce(properties)

슈퍼 속성을 등록하되, 이미 설정된 키를 건너뜁니다.

```typescript
SolapiCRM.registerOnce({
  firstVisit: new Date().toISOString(),
  source: 'google_ads',
});

// 이후 호출은 이를 덮어쓰지 않음
SolapiCRM.registerOnce({
  firstVisit: '2099-01-01', // 건너뜀 (이미 설정됨)
  source: 'facebook_ads',   // 건너뜀 (이미 설정됨)
});
```

**매개변수**:
- `properties` (객체): 아직 존재하지 않으면 설정할 키-값 쌍

---

### unregister(propertyName)

슈퍼 속성을 제거합니다.

```typescript
SolapiCRM.unregister('tempFlag');
```

**매개변수**:
- `propertyName` (문자열): 제거할 키

---

### optOutTracking()

모든 이벤트 추적을 옵트아웃합니다.

```typescript
SolapiCRM.optOutTracking();
// optInTracking()을 호출할 때까지 이벤트가 전송되지 않음
```

**동작**:
- 모든 `track()` 호출이 자동으로 반환됨
- 이벤트 큐 타이머 중지
- 메모리에 저장됨 (저장 안 함; 페이지 새로고침 시 재설정)

**사용 사례**: 사용자 개인정보 보호 설정 또는 GDPR 동의 존중.

---

### optInTracking()

옵트아웃 후 이벤트 추적을 재개합니다.

```typescript
SolapiCRM.optInTracking();
```

**동작**:
- 이벤트 수락 및 전송 재개
- 이벤트 큐 타이머 재시작

---

### hasOptedOut()

현재 옵트아웃 상태를 확인합니다.

```typescript
if (SolapiCRM.hasOptedOut()) {
  console.log('추적이 비활성화됨');
}
```

**반환**: `boolean` — 옵트아웃된 경우 `true`, 추적이 활성인 경우 `false`.

---

### reset()

사용자 세션 식별을 재설정합니다.

```typescript
// 사용자 로그아웃 시
SolapiCRM.reset();
```

**동작**:
- 새로운 익명 ID 생성
- 모든 슈퍼 속성 삭제
- localStorage에서 지속된 레코드 ID 삭제
- 향후 이벤트는 새 익명 ID로 전송됨

**사용 사례**: 사용자 로그아웃 또는 세션 재설정 처리.

---

### flush()

모든 큐 이벤트를 즉시 서버로 전송합니다.

```typescript
await SolapiCRM.flush();
```

**동작**:
- 자동 플러시 간격 무시
- 모든 재시도 후 지수 백오프 최대 3회 재시도 (1초, 2초, 4초)
- 이벤트를 최대 100개 그룹으로 배치
- 모든 재시도 후 지속적 네트워크 오류 시 삭제

**사용 사례**: 페이지 언로드 전에 이벤트가 전송되는지 확인 (자동 처리됨).

---

### destroy()

SDK 인스턴스를 정리하고 삭제합니다.

```typescript
await SolapiCRM.destroy();
```

**동작**:
- 모든 남은 큐 이벤트 플러시
- 패치된 경우 원본 `history.pushState/replaceState` 복원
- 모든 이벤트 리스너 제거
- 타이머 중지
- 내부 상태 삭제

**사용 사례**: 단일 페이지 앱 라우트 변경 또는 앱 언마운트 시 정리.

---

## 일반적인 패턴

### React 통합

```typescript
// App.tsx
import { useEffect } from 'react';
import SolapiCRM from '@solapi/crm-browser';

export function App() {
  useEffect(() => {
    // 마운트 시 초기화
    SolapiCRM.init({
      trackingKey: import.meta.env.VITE_TRACKING_KEY,
      entityId: 'entity_web_app',
      autoPageView: true,
    });

    // 언마운트 시 정리
    return () => {
      SolapiCRM.destroy();
    };
  }, []);

  return <div>Your app</div>;
}
```

### 사용자 인증 흐름

```typescript
// 성공적인 로그인 후
const user = await loginUser(credentials);

const record = await SolapiCRM.createRecord({
  name: user.fullName,
  data: {
    email: user.email,
    role: user.role,
  },
});

await SolapiCRM.identify(record.recordId, {
  email: user.email,
  role: user.role,
});

// 이제 모든 이벤트가 이 사용자와 연결됨
SolapiCRM.track('login_success');
```

### SPA 라우트 네비게이션

```typescript
// 라우터에서 (예: React Router)
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import SolapiCRM from '@solapi/crm-browser';

export function RouteTracker() {
  const location = useLocation();

  useEffect(() => {
    // 각 라우트 변경 시 수동 페이지 뷰
    SolapiCRM.trackPageView();
  }, [location]);

  return null;
}

// 또는 자동 추적을 위해 enableAutoPageView() 사용
SolapiCRM.enableAutoPageView();
```

### GDPR 옵트아웃 동의 배너

```typescript
function ConsentBanner() {
  const handleOptOut = () => {
    localStorage.setItem('tracking_consent', 'denied');
    SolapiCRM.optOutTracking();
  };

  const handleOptIn = () => {
    localStorage.setItem('tracking_consent', 'accepted');
    SolapiCRM.optInTracking();
  };

  return (
    <div className="consent-banner">
      <p>분석을 통해 환경을 개선하기 위해 사용합니다.</p>
      <button onClick={handleOptOut}>거부</button>
      <button onClick={handleOptIn}>승인</button>
    </div>
  );
}

// 앱 로드 시 저장된 동의 존중
const consent = localStorage.getItem('tracking_consent');
if (consent === 'denied') {
  SolapiCRM.optOutTracking();
}
```

### Vue.js 통합

```typescript
// main.ts
import { createApp } from 'vue';
import SolapiCRM from '@solapi/crm-browser';
import App from './App.vue';

const app = createApp(App);

SolapiCRM.init({
  trackingKey: import.meta.env.VITE_TRACKING_KEY,
  entityId: 'entity_vue_app',
});

app.config.globalProperties.$crm = SolapiCRM;
app.mount('#app');
```

### 이벤트 속성 모범 사례

```typescript
// 좋은 예: 명확한 소문자 키, 최소 중첩
SolapiCRM.track('add_to_cart', {
  product_id: '123',
  product_name: 'Laptop',
  price: 1500000,
  currency: 'KRW',
  quantity: 1,
});

// 피해야 할 것: 중첩 객체, 일관성 없는 이름, 과도한 데이터
SolapiCRM.track('add_to_cart', {
  product: {
    id: '123',
    name: 'Laptop',
    pricing: {
      amount: 1500000,
    },
  },
  ProductQuantity: 1,  // 일관성 없는 대소문자
});
```

## 브라우저 호환성

SDK는 최신 브라우저 API가 필요합니다. 최소 지원:

| 브라우저 | 최소 버전 |
|---------|----------|
| Chrome  | 57+      |
| Firefox | 52+      |
| Safari  | 11+      |
| Edge    | 79+      |
| iOS Safari | 11+ |
| Android Chrome | 57+ |

**필수 API**:
- `fetch()`
- `crypto.getRandomValues()`
- `localStorage` (선택; 우아하게 폴백)
- `history.pushState/replaceState` (SPA 추적용)

**우아한 폴백**:
- `localStorage`를 사용할 수 없으면 (개인 모드, iframe 샌드박스) ID는 세션별로 생성됨
- 기록 API 패칭이 실패하면 수동 `trackPageView()` 호출은 여전히 작동
- 네트워크 오류는 포착되고 로깅됨; 호스트 앱이 절대 충돌하지 않음

## TypeScript 지원

내보낸 타입의 완전한 TypeScript 지원:

```typescript
import type { SolapiCRMConfig, TrackEvent } from '@solapi/crm-browser';
import SolapiCRM from '@solapi/crm-browser';

const config: SolapiCRMConfig = {
  trackingKey: 'tk_live_...',
  entityId: 'entity_main',
  debug: true,
};

SolapiCRM.init(config);

interface CustomEvent extends TrackEvent {
  customProperty: string;
}
```

## 디버그 모드

문제를 해결하기 위해 자세한 로깅을 활성화합니다:

```typescript
SolapiCRM.init({
  trackingKey: 'tk_live_...',
  debug: true,
});

// 콘솔 출력:
// [SolapiCRM] Event queued: purchase
// [SolapiCRM] Flushing 5 events...
// [SolapiCRM] Events sent successfully
```

## 보안 고려사항

- **HTTPS만**: SDK는 HTTPS 전송을 강제함 (개발용 localhost 제외)
- **추적 키 보안**: 공개 저장소에 `trackingKey`를 노출하지 마세요. 환경 변수를 사용하세요.
- **프로토타입 오염**: `__proto__`, `constructor`, `prototype` 같은 속성 키는 자동으로 차단됨
- **ID 생성**: 암호학적으로 안전한 ID를 위해 `crypto.getRandomValues()`를 거부 샘플링과 함께 사용
- **민감한 데이터 없음**: 이벤트 속성에 비밀번호, 신용카드 번호 또는 PII를 보내지 마세요

**모범 사례**:
```typescript
// 환경 변수에 추적 키 저장
const config = {
  trackingKey: process.env.REACT_APP_TRACKING_KEY,
};

// 이렇게 하지 마세요:
const config = {
  trackingKey: 'tk_live_hardcoded_key', // 소스 코드에 표시됨
};
```

## 성능 팁

1. **배치 이벤트**: 개별 요청 대신 이벤트를 큐잉하고 주기적으로 플러시합니다
2. **슈퍼 속성 제한**: 필수 전역 속성만 등록합니다
3. **적절한 배치 크기 사용**: 네트워크 오버헤드와 큐 메모리의 균형

```typescript
SolapiCRM.init({
  trackingKey: 'tk_live_...',
  batchSize: 20,        // 20개 이벤트 후 플러시 (기본값: 10)
  flushInterval: 10000, // 10초마다 플러시 (기본값: 5초)
});
```

4. **정리**: SDK가 더 이상 필요하지 않을 때 `destroy()` 호출

## 저장소

SDK는 브라우저 localStorage를 사용하여 다음을 저장합니다:

| 키 | 값 | 목적 |
|----|------|------|
| `_solapi_anon_id` | 무작위로 생성된 ID | 지속적 익명 사용자 ID |
| `_solapi_record_id` | `identify()`의 레코드 ID | 지속적 식별 사용자 |

**참고**: `localStorage`를 사용할 수 없으면 저장소가 사용되지 않음 (예: 개인 모드, 샌드박스가 있는 iframe).

## 오류 처리

SDK는 호스트 애플리케이션을 절대 손상시키지 않도록 설계되었습니다:

```typescript
try {
  // 항상 안전 — 오류는 포착되고 로깅됨
  SolapiCRM.track('event_name', { customData: true });
} catch (error) {
  // 이것은 드물게 발생 (init에서만)
  console.error('SolapiCRM error:', error);
}
```

**네트워크 실패**: 이벤트는 지수 백오프로 최대 3회 재시도됩니다. 지속적 실패 시 이벤트가 삭제되고 로깅됨 (디버그가 활성화된 경우).

**초기화**: `trackingKey`가 누락된 경우에만 `init()`이 오류를 발생시킵니다.

## 변경 로그

### 0.1.0 (초기 릴리스)
- 사용자 정의 속성을 사용한 이벤트 추적
- SPA를 위한 페이지 뷰 추적
- 레코드 생성 및 식별
- 슈퍼 속성 지원
- GDPR 준수 옵트인/옵트아웃
- 자동 배칭 및 재시도 로직
- TypeScript 지원
- 브라우저 SDK 배포 (IIFE + ESM)

## 라이선스

MIT