vc-cdp-web-sdk
v0.1.4
Published
CDP SDK for Web - Customer Data Platform tracking for browsers and SSR
Maintainers
Readme
vc-cdp-web-sdk
CDP SDK cho Web — tracking events va user profiles cho he thong Customer Data Platform noi bo VietCredit.
Ho tro Browser (Vanilla JS, React, Vue, Angular, ...) va SSR (Next.js, Nuxt.js, ...).
Muc luc
Cai dat
npm install vc-cdp-web-sdkCDN (khong can build tool)
<!-- Load SDK tu CDN -->
<script src="https://unpkg.com/vc-cdp-web-sdk/dist/vc-cdp-web-sdk.umd.min.js"></script>const { CdpWebSdk } = VcCdpWebSdk;Khoi tao
import { CdpWebSdk } from 'vc-cdp-web-sdk';
const cdp = new CdpWebSdk({
apiKey: 'YOUR_API_KEY',
secretKey: 'YOUR_SECRET_KEY',
source: 'DOP',
serviceName: 'VC_APP',
// Tuy chon
baseUrl: 'https://ingestlog.vietcredit.com.vn', // mac dinh: staging
isTest: false,
debug: false,
batchSize: 10, // gui batch khi du n events
batchInterval: 5000, // hoac sau n ms
enableEncryption: true, // ma hoa PII bang AES-256-CBC
autoTrackPageView: false, // tu track page_view khi URL thay doi (SPA)
deviceId: 'custom-device-id', // tuy chon: set device_id tuy chinh
});Luu y: Khoi tao la synchronous — khong can
await. SDK dunglocalStoragevasessionStoragenen hoat dong ngay lap tuc.
Cau hinh day du
| Tham so | Kieu | Mac dinh | Mo ta |
|---------|------|----------|-------|
| apiKey | string | — | Bat buoc. API Key do CDP cap |
| secretKey | string | — | Bat buoc. Secret Key de ky HMAC va ma hoa AES |
| source | string | — | Bat buoc. Kenh tich hop (khai bao voi CDP truoc) |
| serviceName | string | — | Bat buoc. VC_APP, DOP, LOS, LMS |
| baseUrl | string | Staging URL | URL API CDP (khong co /v1) |
| isTest | boolean | false | Danh dau moi truong test |
| debug | boolean | false | In log ra console |
| batchSize | number | 10 | So events toi da trong 1 batch |
| batchInterval | number | 5000 | Thoi gian (ms) giua cac lan flush tu dong |
| enableEncryption | boolean | true | Ma hoa PII bang AES-256-CBC |
| autoTrackPageView | boolean | false | Tu track page view khi URL thay doi (SPA) |
| deviceId | string | Auto UUID | Device ID tuy chinh. Neu khong set, SDK tu sinh UUID va luu vao localStorage |
Moi truong
| Moi truong | baseUrl |
|------------|---------|
| Staging | https://stg-ingestlog.vietcredit.com.vn |
| Production | https://ingestlog.vietcredit.com.vn |
Luong hoat dong
Trang load
+-> new CdpWebSdk(config)
| - device_id: dung config.deviceId neu co, khong thi load/tao tu localStorage
| - Load/tao anonymous_id -> localStorage (persist mai)
| - Load/tao session_id -> sessionStorage (per tab)
| - Load profile_id -> localStorage (neu da login)
| - Parse UTM params tu URL tu dong
|
+- User CHUA login
| +-> cdp.track() / cdp.page()
| profile_id = null, gan anonymous_id + device_id + session_id
|
+- User login
+-> cdp.identifyUser(userId, traits)
+-> POST /v1/profiles/track -> nhan profile_id
+-> cdp.track() gan profile_id vao moi eventStorage scope:
| Key | localStorage | sessionStorage |
|-----|-------------|----------------|
| device_id | Vinh vien (hoac tu config.deviceId) | - |
| anonymous_id | Vinh vien | - |
| profile_id | Vinh vien | - |
| session_id | - | Per tab (mat khi dong tab) |
User / Profile
identifyUser — Dang nhap
Goi sau khi user dang nhap thanh cong. SDK goi POST /v1/profiles/track, nhan profile_id tu CDP va luu vao localStorage.
await cdp.identifyUser(userId, traits?, partyType?)| Tham so | Kieu | Bat buoc | Mo ta |
|---------|------|----------|-------|
| userId | string | Co | Ma user trong he thong noi bo |
| traits | UserTraits | Khong | Thong tin nhan khau hoc |
| partyType | 'PERSON' or 'ORG' | Khong | Mac dinh 'PERSON' |
// Ca nhan
await cdp.identifyUser('user_001', {
phone: '0901234567',
email: '[email protected]',
full_name: 'Nguyen Van A',
gender: 'M',
dob: '1990-01-15',
idcard: '012345678901',
address: '123 Le Loi, Q.1, TP.HCM',
occupation: 'Ky su phan mem',
nationality: 'Vietnamese',
});
// To chuc / Doanh nghiep
await cdp.identifyUser('org_001', {
full_name: 'Cong ty TNHH ABC',
phone: '0281234567',
email: '[email protected]',
}, 'ORG');Trait Setters — Cap nhat tung thuoc tinh
Moi setter goi ngay POST /v1/profiles/track (type: update). Co the noi chuoi (chainable).
cdp
.add_first_name('Van A')
.add_last_name('Nguyen')
.add_user_name('Nguyen Van A')
.add_email('[email protected]')
.add_mobile('0901234567')
.add_gender('M') // 'M' | 'F' | 'O'
.add_birthday('1990-01-15') // hoac new Date('1990-01-15')
.add_idcard('012345678901')
.add_old_idcard('123456789')
.add_address('123 Le Loi, Q.1, TP.HCM')
.add_occupation('Ky su phan mem')
.add_nationality('Vietnamese')
.add_marital_status('single')
.add_religion('Buddhist')
.add_zalo_id('zalo_user_001')
.add_tiktok_id('tiktok_user_001');| Method | Truong | Ghi chu |
|--------|--------|---------|
| add_first_name(v) | first_name | - |
| add_last_name(v) | last_name | - |
| add_user_name(v) | full_name | - |
| add_email(v) | email | PII — tu dong ma hoa |
| add_mobile(v) | phone | PII — tu dong ma hoa |
| add_gender(v) | gender | 'M', 'F', 'O' |
| add_birthday(v) | dob | Date hoac 'yyyy-MM-dd' |
| add_idcard(v) | idcard | PII — tu dong ma hoa |
| add_old_idcard(v) | old_idcard | PII — tu dong ma hoa |
| add_address(v) | address | PII — tu dong ma hoa |
| add_occupation(v) | occupation | - |
| add_nationality(v) | nationality | - |
| add_marital_status(v) | marital_status | - |
| add_religion(v) | religion | PII — tu dong ma hoa |
| add_zalo_id(v) | zalo_id | - |
| add_tiktok_id(v) | tiktok_id | - |
| add_user_attribute(name, val) | custom | Bat ky truong nao |
| add_user_attributes(obj) | nhieu truong | Batch update |
| setUserAttributes(obj) | nhieu truong | Alias cua tren |
updateProfile — Force-update
Cap nhat profile theo profile_id ma khong trigger identity resolution. Goi POST /v1/profiles/update.
await cdp.updateProfile({
traits: {
kyc_status: 'verified',
mps_verified: true,
},
consents: [
{ consent_type: 'marketing_sms', status: true, consent_date: '2025-06-01' },
],
});
// Cap nhat profile_id khac
await cdp.updateProfile({
profile_id: 'cdp_profile_xyz',
traits: { occupation: 'Giam doc' },
});Consents — Dong y su dung du lieu
cdp.setConsents([
{ consent_type: 'marketing_sms', status: true, consent_date: '2025-06-01' },
{ consent_type: 'marketing_email', status: false },
{ consent_type: 'data_sharing', status: true, consent_date: '2025-06-01' },
]);| Truong | Kieu | Bat buoc | Mo ta |
|--------|------|----------|-------|
| consent_type | string | Co | Loai consent |
| status | boolean | Co | true = dong y |
| consent_date | string | Khong | Ngay dong y (yyyy-MM-dd) |
Scores — Diem tin dung / rui ro
cdp.setScores([
{ score_type: 'credit_score', score_value: 720, score_date: '2025-06-01' },
{ score_type: 'risk_score', score_value: 82 },
]);Products — San pham dang su dung
cdp.setProducts([
{ product_code: 'LOAN_CONSUMER', product_name: 'Vay tieu dung', status: 'active' },
{ product_code: 'CREDIT_CARD', product_name: 'The tin dung', status: 'pending' },
]);Metadata va Campaign
cdp.setMetadata({
schema_version: '2.0',
app_flavor: 'production',
referral_code: 'REF123',
});
// SDK tu parse UTM tu URL. Override thu cong:
cdp.setCampaign({
utm_source: 'facebook',
utm_medium: 'cpc',
utm_campaign: 'summer_loan_2025',
utm_term: 'vay_tieu_dung',
utm_content: 'banner_top',
});getUserIdentities
const ids = cdp.getUserIdentities();
// {
// user_id: 'user_001',
// profile_id: '195103072953831424',
// anonymous_id: 'uuid-...',
// device_id: 'uuid-...',
// session_id: 'uuid-...',
// }
cdp.getProfileId(); // profile_id tu CDP
cdp.getDeviceId(); // persistent per browser
cdp.getSessionId(); // per tab (sessionStorage)
cdp.getUserAttribute('phone'); // lay trait cu thedestroy_session — Dang xuat
await cdp.destroy_session();
// hoac alias:
await cdp.reset();| Field | Hanh dong |
|-------|-----------|
| userId, profileId, userTraits | Xoa |
| consents, scores, products, campaign | Xoa |
| anonymous_id | Tao moi, luu localStorage |
| session_id | Tao moi, luu sessionStorage |
| device_id | Giu nguyen |
Event Tracking
track — Custom event
cdp.track(eventName, properties?)cdp.track('button_click');
cdp.track('loan_viewed', {
loan_id: 'LOAN_001',
amount: 50000000,
term: 24,
type: 'consumer',
});
cdp.track('form_submitted', {
form: 'loan_application',
success: true,
});page — Page view
Track luot xem trang. Tu dong ghi url, path, referrer, title.
cdp.page(pageName?, properties?)cdp.page(); // tu dong capture url/referrer/title
cdp.page('Trang chu');
cdp.page('Chi tiet san pham', {
product_code: 'LOAN_CONSUMER',
campaign: 'SUMMER_2025',
});screen — Screen view (SPA)
cdp.screen('DashboardScreen');
cdp.screen('LoanDetailScreen', { loan_id: 'LOAN_001' });Auto Page View (SPA)
Bat autoTrackPageView: true de SDK tu track page_view moi khi URL thay doi — khong can goi cdp.page() thu cong.
SDK hook vao:
history.pushState(React Router, Vue Router, Next.js)history.replaceStatewindow.popstate(trinh duyet back/forward)
Luu y: Khi dung SSR (Next.js, Nuxt.js), chi init SDK o phia client de tranh loi
window is not defined.
Batch Processing
Events khong gui ngay tung cai ma duoc gom vao queue roi flush theo dieu kien:
| Dieu kien | Mo ta |
|-----------|-------|
| batchSize dat nguong | Mac dinh 10 events |
| batchInterval timeout | Mac dinh 5000 ms |
| Tab dong (beforeunload) | Flush dong bo |
| Tab an (visibilitychange) | Flush khi chuyen sang background |
const cdp = new CdpWebSdk({
batchSize: 20, // gui khi co 20 events
batchInterval: 3000, // hoac sau 3 giay
});flush — Gui ngay
await cdp.flush();document.querySelector('#submit-btn').addEventListener('click', async () => {
cdp.track('form_submitted', { form: 'loan_application' });
await cdp.flush(); // dam bao event duoc gui
window.location.href = '/thank-you';
});destroy — Don dep SDK
cdp.destroy();Xoa batch interval timer, xoa history listener, flush queue con lai. Khong xoa user data.
Bao mat
HMAC-SHA256 Signature
Moi request den CDP API deu duoc ky tu dong.
Headers duoc gui:
X-Api-Key: <apiKey>
X-Source: <source>
X-Timestamp: <unix_timestamp_ms>
X-Signatures: <base64_signature>
X-Trace-Id: <uuid>
Content-Type: application/json
isTest: falseCong thuc:
signature = Base64( HMAC-SHA256(secretKey, message) )
message = source + "|" + timestamp + "|" + JSON(payload)| Thanh phan | Mo ta |
|------------|-------|
| secretKey | Secret Key duoc CDP cap |
| source | Kenh tich hop (vi du: DOP) |
| timestamp | Unix timestamp tinh bang milliseconds (Date.now()) |
| payload | Body JSON string (GET request: chuoi rong) |
AES-256-CBC Encryption
Khi enableEncryption: true, cac truong PII nhay cam trong traits duoc ma hoa truoc khi gui.
Cac truong PII bi ma hoa:
| Truong | Mo ta |
|--------|-------|
| phone | So dien thoai |
| email | Email |
| idcard | CMND/CCCD moi |
| old_idcard | CMND cu |
| address | Dia chi |
| religion | Ton giao |
Cong thuc:
encrypted = Base64( IV[16 bytes] || AES_CBC_PKCS7( key, IV, plaintext ) )
key = SHA256( secretKey + timestamp )
IV = random 16 bytes
plaintext = JSON.stringify(pii_traits_object)Luu y: Khong bat tren dev/staging neu server chua cau hinh giai ma. Dung
enableEncryption: falsekhi test.
Best Practices
- Dung key khac nhau cho moi moi truong (dev / staging / production).
- Rotate key dinh ky theo chinh sach CDP team.
- Bat
debug: falsetren production build. - Luon dung
https://chobaseUrl.
// Development
const cdp = new CdpWebSdk({ enableEncryption: false, debug: true, isTest: true });
// Production
const cdp = new CdpWebSdk({ enableEncryption: true, debug: false });Tich hop Framework
React / Next.js
React (Create React App / Vite)
// src/lib/cdp.ts
import { CdpWebSdk } from 'vc-cdp-web-sdk';
export const cdp = new CdpWebSdk({
apiKey: process.env.REACT_APP_CDP_API_KEY!,
secretKey: process.env.REACT_APP_CDP_SECRET_KEY!,
source: 'DOP',
serviceName: 'VC_APP',
baseUrl: process.env.REACT_APP_CDP_BASE_URL,
debug: process.env.NODE_ENV === 'development',
autoTrackPageView: true,
});// src/hooks/useAuth.ts
import { cdp } from '../lib/cdp';
export function useLogin() {
const login = async (phone: string, password: string) => {
const user = await authApi.login(phone, password);
await cdp.identifyUser(user.id, {
phone: user.phone,
email: user.email,
full_name: user.name,
});
return user;
};
const logout = async () => {
await authApi.logout();
await cdp.destroy_session();
};
return { login, logout };
}# .env.local
REACT_APP_CDP_API_KEY=YOUR_API_KEY
REACT_APP_CDP_SECRET_KEY=YOUR_SECRET_KEY
REACT_APP_CDP_BASE_URL=https://stg-ingestlog.vietcredit.com.vnNext.js (App Router)
SDK phai chay o client side vi can localStorage va window.
// src/lib/cdp.ts
'use client';
import { CdpWebSdk } from 'vc-cdp-web-sdk';
let _cdp: CdpWebSdk | null = null;
export function getCdp(): CdpWebSdk {
if (typeof window === 'undefined') {
return { track: () => {}, page: () => {}, identifyUser: async () => {} } as any;
}
if (!_cdp) {
_cdp = new CdpWebSdk({
apiKey: process.env.NEXT_PUBLIC_CDP_API_KEY!,
secretKey: process.env.NEXT_PUBLIC_CDP_SECRET_KEY!,
source: 'DOP',
serviceName: 'VC_APP',
baseUrl: process.env.NEXT_PUBLIC_CDP_BASE_URL,
debug: process.env.NODE_ENV === 'development',
autoTrackPageView: true,
});
}
return _cdp;
}// src/components/CdpProvider.tsx
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import { getCdp } from '../lib/cdp';
export function CdpProvider({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
useEffect(() => {
getCdp().page(document.title);
}, [pathname]);
return children;
}// src/app/layout.tsx
import { CdpProvider } from '../components/CdpProvider';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<CdpProvider>{children}</CdpProvider>
);
}// src/hooks/useCdp.ts
'use client';
import { getCdp } from '../lib/cdp';
export function useCdp() { return getCdp(); }# .env.local
NEXT_PUBLIC_CDP_API_KEY=YOUR_API_KEY
NEXT_PUBLIC_CDP_SECRET_KEY=YOUR_SECRET_KEY
NEXT_PUBLIC_CDP_BASE_URL=https://stg-ingestlog.vietcredit.com.vnVue / Nuxt.js
Vue 3 (Vite / Vue CLI)
// src/plugins/cdp.ts
import { App } from 'vue';
import { CdpWebSdk } from 'vc-cdp-web-sdk';
export const cdp = new CdpWebSdk({
apiKey: import.meta.env.VITE_CDP_API_KEY,
secretKey: import.meta.env.VITE_CDP_SECRET_KEY,
source: 'DOP',
serviceName: 'VC_APP',
baseUrl: import.meta.env.VITE_CDP_BASE_URL,
debug: import.meta.env.DEV,
autoTrackPageView: true,
});
export const CdpPlugin = {
install(app: App) {
app.config.globalProperties.$cdp = cdp;
app.provide('cdp', cdp);
},
};// src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { CdpPlugin } from './plugins/cdp';
createApp(App).use(CdpPlugin).mount('#app');// src/composables/useCdp.ts
import { inject } from 'vue';
import type { CdpWebSdk } from 'vc-cdp-web-sdk';
export function useCdp(): CdpWebSdk {
return inject('cdp') as CdpWebSdk;
}// src/router/index.ts — track page view qua Vue Router
router.afterEach((to) => {
cdp.page(String(to.name ?? to.path));
});# .env.local
VITE_CDP_API_KEY=YOUR_API_KEY
VITE_CDP_SECRET_KEY=YOUR_SECRET_KEY
VITE_CDP_BASE_URL=https://stg-ingestlog.vietcredit.com.vnNuxt.js 3
// plugins/cdp.client.ts (file .client.ts chi chay o browser)
import { CdpWebSdk } from 'vc-cdp-web-sdk';
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig();
const cdp = new CdpWebSdk({
apiKey: config.public.cdpApiKey,
secretKey: config.public.cdpSecretKey,
source: 'DOP',
serviceName: 'VC_APP',
baseUrl: config.public.cdpBaseUrl,
debug: process.env.NODE_ENV === 'development',
});
const router = useRouter();
router.afterEach((to) => {
cdp.page(String(to.name ?? to.path));
});
return { provide: { cdp } };
});// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
cdpApiKey: process.env.NUXT_PUBLIC_CDP_API_KEY,
cdpSecretKey: process.env.NUXT_PUBLIC_CDP_SECRET_KEY,
cdpBaseUrl: process.env.NUXT_PUBLIC_CDP_BASE_URL,
},
},
});# .env
NUXT_PUBLIC_CDP_API_KEY=YOUR_API_KEY
NUXT_PUBLIC_CDP_SECRET_KEY=YOUR_SECRET_KEY
NUXT_PUBLIC_CDP_BASE_URL=https://stg-ingestlog.vietcredit.com.vnVanilla JS / CDN
Script tag
Them SDK vao trang web bang the script:
<!-- Load SDK -->
<script src="https://unpkg.com/vc-cdp-web-sdk/dist/vc-cdp-web-sdk.umd.min.js"></script>Sau do khoi tao va su dung:
var CdpWebSdk = VcCdpWebSdk.CdpWebSdk;
var cdp = new CdpWebSdk({
apiKey: 'YOUR_API_KEY',
secretKey: 'YOUR_SECRET_KEY',
source: 'DOP',
serviceName: 'VC_APP',
baseUrl: 'https://ingestlog.vietcredit.com.vn',
});
// Track page view
cdp.page(document.title);
// Track su kien
document.getElementById('apply-btn').addEventListener('click', function() {
cdp.track('apply_clicked', { product_code: this.dataset.product });
});
// Dang nhap
cdp.identifyUser(userId, {
phone: '0901234567',
email: '[email protected]',
full_name: 'Nguyen Van A',
});
// Dang xuat
cdp.destroy_session();Multi-page Website (MPA)
Them vao base template hoac moi trang:
<!-- Load SDK -->
<script src="https://unpkg.com/vc-cdp-web-sdk/dist/vc-cdp-web-sdk.umd.min.js"></script>window.cdp = new VcCdpWebSdk.CdpWebSdk({
apiKey: 'YOUR_API_KEY',
secretKey: 'YOUR_SECRET_KEY',
source: 'DOP',
serviceName: 'VC_APP',
baseUrl: 'https://ingestlog.vietcredit.com.vn',
});
window.cdp.page(document.title);SDK nhan ra cung user qua device_id (localStorage) du navigate giua cac trang.
ES Modules
// src/analytics.js
import { CdpWebSdk } from 'vc-cdp-web-sdk';
const cdp = new CdpWebSdk({
apiKey: process.env.CDP_API_KEY,
secretKey: process.env.CDP_SECRET_KEY,
source: 'DOP',
serviceName: 'VC_APP',
});
export default cdp;API Reference
Constructor
new CdpWebSdk(config: CdpWebConfig)
interface CdpWebConfig {
apiKey: string;
secretKey: string;
source: string;
serviceName: string;
baseUrl?: string;
isTest?: boolean;
debug?: boolean;
batchSize?: number;
batchInterval?: number;
enableEncryption?: boolean;
autoTrackPageView?: boolean;
deviceId?: string;
}Profile Methods
async identifyUser(userId: string, traits?: UserTraits, partyType?: 'PERSON' | 'ORG'): Promise<void>
async identify(userId: string, traits?: UserTraits): Promise<void> // alias
async updateProfile(payload: ProfileUpdatePayload): Promise<void>Trait Setter Methods (chainable)
add_first_name(v: string): this
add_last_name(v: string): this
add_user_name(v: string): this
add_email(v: string): this
add_mobile(v: string): this
add_gender(v: 'M' | 'F' | 'O'): this
add_birthday(v: string | Date): this
add_idcard(v: string): this
add_old_idcard(v: string): this
add_address(v: string): this
add_occupation(v: string): this
add_nationality(v: string): this
add_marital_status(v: string): this
add_religion(v: string): this
add_zalo_id(v: string): this
add_tiktok_id(v: string): this
add_user_attribute(name: string, value: unknown): this
add_user_attributes(attrs: Record<string, unknown>): this
setUserAttributes(attrs: Partial<UserTraits>): thisConsent / Score / Product / Campaign Methods
setConsents(consents: ConsentInfo[]): void
setScores(scores: ScoreInfo[]): void
setProducts(products: ProductInfo[]): void
setMetadata(metadata: Record<string, unknown>): void
setCampaign(campaign: CampaignInfo): voidEvent Methods
track(eventName: string, properties?: Record<string, unknown>): void
page(pageName?: string, properties?: Record<string, unknown>): void
screen(screenName: string, properties?: Record<string, unknown>): void
async flush(): Promise<void>Session and Identity Methods
getUserIdentities(): UserIdentities
getProfileId(): string | null
getDeviceId(): string
getSessionId(): string
getUserAttribute(name: keyof UserTraits): unknown
async destroy_session(): Promise<void>
async reset(): Promise<void> // alias
destroy(): void // cleanup timersTypes
interface UserTraits {
full_name?: string;
first_name?: string;
last_name?: string;
phone?: string; // PII - encrypted
email?: string; // PII - encrypted
gender?: 'M' | 'F' | 'O';
dob?: string; // 'yyyy-MM-dd'
idcard?: string; // PII - encrypted
old_idcard?: string; // PII - encrypted
address?: string; // PII - encrypted
occupation?: string;
nationality?: string;
marital_status?: string;
religion?: string; // PII - encrypted
zalo_id?: string;
tiktok_id?: string;
mps_verified?: boolean;
kyc_status?: string;
[key: string]: unknown; // truong tu do
}
interface ConsentInfo {
consent_type: string;
status: boolean;
consent_date?: string; // 'yyyy-MM-dd'
}
interface ScoreInfo {
score_type: string;
score_value: number;
score_date?: string;
}
interface ProductInfo {
product_code: string;
product_name?: string;
status?: string;
}
interface UserIdentities {
user_id?: string;
profile_id?: string;
anonymous_id: string;
device_id: string;
session_id: string;
}
interface ProfileUpdatePayload {
profile_id?: string;
traits?: UserTraits;
consents?: ConsentInfo[];
scores?: ScoreInfo[];
products?: ProductInfo[];
metadata?: Record<string, unknown>;
}