پرش به محتوا
مستندات
EN
ورود به پنل
شروع
  • نقشهٔ مستندات
  • راه‌اندازی سریع
  • مفهوم‌ها
جمع‌آوری داده
  • تعریف رویداد
  • فرهنگ‌نامهٔ رویدادها
  • گذاشتن رویداد
  • ادغام هویت
  • SDK وب
  • SDK اندروید
  • ثبت دستگاه
  • سرور به سرور
  • کاتالوگ محصولات
  • وب‌هوک
مخاطب و پیام‌رسانی
  • سگمنت
  • سناریو
  • پیام تراکنشی
  • رضایت و سقف
  • پیام درون‌برنامه‌ای
تحلیل و خروجی
  • گزارش و خروجی
مرجع توسعه‌دهنده
  • مرجع API
    • نقاط ورود داده
    • API مدیریتی
  • کدهای خطا
  • سقف‌ها
  • OpenAPI
ابزارهای توسعه
  • سرور MCP
  • کار با عامل
حریم خصوصی و تغییرات
  • داده‌های شخصی
  • نسخه و تغییرها

SDK وب

نصب روی سایت، همهٔ متدها، تنظیمات، صف آفلاین، و اعلان مرورگر.

SDK وب رویدادها را در مرورگر جمع می‌کند، تا وقتی شبکه برنگشته روی localStorage نگه می‌دارد، و هر پیام را با یک شناسه ثابت می‌فرستد تا ارسال دوباره روی شبکه بی‌کیفیت، خرید کسی را دو بار نشمارد. همان فایل، اعلان مرورگر و پیام‌های درون‌سایت را هم راه می‌اندازد.

کتابخانه در هر پیام خودش را با library.name = "segmentic-js" و library.version = "0.1.0" معرفی می‌کند.

#نصب

باندل از همان مبدائی سرو می‌شود که رویدادها به آن پست می‌شوند. این عمدی است: یک ورودی در Content-Security-Policy مشتری لازم می‌شود، نه دو تا.

https://in.segmentic.net/sdk/segmentic.js

#با تگ اسکریپت

این همان کدی است که صفحه «اتصال» در پنل تولید می‌کند. کلید نوشتن (wk_seg_...) عمومی است و قرار است در سورس صفحه دیده شود.

HTML
<script src="https://in.segmentic.net/sdk/segmentic.js"></script>
<script>
  Segmentic.init({
    writeKey: "wk_seg_...",
    apiHost: "https://in.segmentic.net"
  });
  Segmentic.page();
</script>

روی توسعه لوکال، باندل را دشبورد از پوشه public خودش سرو می‌کند و کالکتور جای دیگری است:

HTML
<script src="http://localhost:3000/sdk/segmentic.js"></script>
<script>
  Segmentic.init({
    writeKey: "wk_seg_...",
    apiHost: "http://localhost:8080"
  });
  Segmentic.page();
</script>

نسخه‌ای از این آدرس وجود ندارد. مسیر /sdk/segmentic.js همیشه ساخت جاری را می‌دهد و هیچ آدرس نسخه‌داری برای پین‌کردن منتشر نشده است. هش integrity هم منتشر نشده، پس اگر سیاست امنیتی شما Subresource Integrity اجباری دارد، باید خودتان فایل را میزبانی کنید و هش را خودتان بسازید.

#با باندلر

بسته npm وجود ندارد. نام @segmentic/web در package.json مخزن هست، ولی روی هیچ رجیستری‌ای منتشر نشده است، هیچ مرحله‌ای در CI آن را بیلد یا منتشر نمی‌کند، و npm install @segmentic/web شکست می‌خورد.

تب npm در صفحه «اتصال» پنل هنوز همان import segmentic from "@segmentic/web" را نشان می‌دهد. آن قطعه کد امروز اجرا نمی‌شود. تا وقتی بسته منتشر نشده، تنها مسیر پشتیبانی‌شده تگ اسکریپت است.

اگر پروژه شما ناچار است ماژول import کند، فایل ESM را از همان باندل بگیرید و کنار کد خودتان بگذارید. تعریف تایپ‌ها (index.d.ts) روی هیچ آدرس عمومی‌ای سرو نمی‌شود، پس با این مسیر TypeScript تایپ نمی‌گیرد.

#راه‌اندازی و همه گزینه‌ها

writeKey و apiHost اجباری‌اند و نبودشان استثنا پرتاب می‌کند: segmentic: writeKey is required و segmentic: apiHost is required. این تنها جایی است که SDK خطا پرتاب می‌کند. اسلش‌های انتهایی apiHost حذف می‌شوند.

گزینهنوعپیش‌فرضکاری که می‌کند
writeKeystringندارد، اجباریکلید نوشتن از پنل. عمومی است
apiHoststringندارد، اجباریآدرس پایه کالکتور
batchSizenumber20وقتی این تعداد پیام در صف جمع شد، بی‌درنگ ارسال کن
flushIntervalnumber میلی‌ثانیه10000حداقل هر این مقدار یک بار ارسال کن
maxQueueSizenumber500چند پیام حق دارند آفلاین روی دیسک منتظر بمانند
maxRetriesnumber10سقف رشد فاصله تلاش دوباره. سقف نگهداری داده نیست
autoContextbooleantrueجمع‌کردن خودکار صفحه، زبان، صفحه‌نمایش و منطقه زمانی
autoPageViewbooleantrueیک پیام page هنگام init() بفرست
respectDoNotTrackbooleantrueتنظیم Do Not Track مرورگر را رعایت کن
onsitebooleantrueکمپین‌های درون‌سایت را بگیر و بکش
debugbooleanfalseفعالیت SDK را با پیشوند [segmentic] در کنسول بنویس
sessionTimeoutnumber میلی‌ثانیه1800000فاصله بی‌کاری که بعدش شناسه نشست عوض می‌شود
now() => number() => Date.now()منبع زمان. برای تست
fetchImpltypeof fetchglobalThis.fetchپیاده‌سازی fetch. برای تست

onsite از قصد روشن است: کمپین درون‌سایت با دست منتشر می‌شود، و مشتری‌ای که یکی منتشر کند و روی سایتش چیزی نبیند هیچ راهی ندارد بفهمد نصب خراب است یا فهرست کمپین‌ها خالی.

init() به ترتیب این کارها را می‌کند: ساخت انبار و آزمایش localStorage با یک نوشتن واقعی، خواندن صف از دیسک، خواندن یا شروع نشست، خواندن وضعیت انصراف و Do Not Track، خواندن یا ساختن شناسه ناشناس، نصب قلاب‌های چرخه عمر، زمان‌بندی ارسال دوره‌ای، ثبت کلیک کمپین، سپس page() اگر autoPageView روشن باشد، سپس یک flush() فوری برای تخلیه هرچه آفلاین جمع شده، و در آخر شروع درون‌سایت.

ثبت کلیک کمپین قبل از بازدید صفحه اجرا می‌شود تا کلیک اولین چیز ثبت‌شده باشد و بازدید صفحه بعدی خودش انتساب را همراه داشته باشد.

صدا زدن دوباره init() اول کلاینت قبلی را با close() می‌بندد.

autoPageView فقط یک بار، داخل init()، بازدید صفحه می‌فرستد. SDK هیچ قلابی روی pushState، replaceState یا popstate نصب نمی‌کند. توضیح تایپ Options می‌گوید «on init and on history navigation» و این نیمه دومش در کد وجود ندارد. در یک اپ تک‌صفحه‌ای بعد از هر تغییر مسیر خودتان Segmentic.page() را صدا بزنید.

#متدها

TypeScript
init(options: Options): SegmenticClient
track(event: string, properties?: Properties, context?: Context): void
identify(userId: string, traits?: Traits, context?: Context): void
page(name?: string, properties?: Properties, context?: Context): void
screen(name: string, properties?: Properties, context?: Context): void
alias(previousId: string, context?: Context): void
reset(): void
flush(): Promise<void>
optOut(): void
optIn(): void
isOptedOut(): boolean
getAnonymousId(): string | null
getUserId(): string | null
stats(): Stats | null
subscribeToPush(options: PushOptions): Promise<PushResult>
unsubscribeFromPush(): Promise<boolean>
pushPermission(): NotificationPermission | "unsupported"

track با نام رویداد خالی، کاری نمی‌کند و فقط در کنسول لاگ می‌گذارد. همین‌طور identify با شناسه کاربر خالی. هیچ‌کدام خطا پرتاب نمی‌کنند، چون یک فراخوانی آنالیتیکس هرگز نباید چیزی باشد که صفحه پرداخت مشتری را می‌شکند. یازده متد از این‌ها اگر قبل از init() صدا زده شوند، [segmentic] <method>() called before init(); ignoring را در کنسول می‌نویسند و برمی‌گردند: track، identify، page، screen، alias، reset، flush، optOut، optIn، subscribeToPush و unsubscribeFromPush. پنج متدی که فقط یک مقدار می‌خوانند، قبل از init() هیچ صدایی ندارند: isOptedOut() مقدار false می‌دهد، getAnonymousId() و getUserId() و stats() مقدار null می‌دهند، و pushPermission() مقدار "unsupported" می‌دهد. آن false از همه مهم‌تر است، چون «انصراف نداده» خوانده می‌شود و هیچ هشداری نمی‌گوید که SDK اصلا راه نیفتاده بود.

identify سه کار دیگر هم می‌کند. اول، اگر این اولین شناسایی بعد از مرور ناشناس باشد و شناسه با قبلی فرق کند، یک پیام alias قبل از identify در صف می‌گذارد؛ بدون آن کل تاریخچه پیش از ورود یتیم می‌شود و هر قیفی که از مرز ورود رد شود عدد اشتباه گزارش می‌دهد. دوم، ویژگی‌های اسکالر را در localStorage مسطح می‌کند تا تارگتینگ درون‌سایت بتواند رویشان شرط بگذارد؛ هر مقدار از نوع object و هر null و undefined کنار گذاشته می‌شود و بقیه با String() به رشته تبدیل می‌شوند. سوم، refreshOnsite() را صدا می‌زند، چون بازدیدکننده واردشده ممکن است حالا با کمپینی جور دربیاید که ناشناس درنمی‌آمد.

page(name) نام را هم به عنوان event می‌فرستد و هم داخل properties.name کپی می‌کند.

screen(name) پیام نوع screen می‌فرستد. برای وب page() درست است؛ screen برای هم‌شکل‌ماندن با SDKهای موبایل وجود دارد.

reset() شناسه کاربر را پاک می‌کند، یک شناسه ناشناس تازه می‌سازد، نشست را می‌اندازد و انتساب کمپین ذخیره‌شده را هم دور می‌ریزد (روی کامپیوتر مشترک، خرید نفر بعدی نباید به پیامی که نفر قبلی گرفته نسبت داده شود). پرچم «پیش‌تر اینجا بوده» را از قصد پاک نمی‌کند: خروج از حساب، کسی را بازدیدکننده تازه نمی‌کند.

flush() وقتی resolve می‌شود که تلاش تمام شده باشد، نه وقتی چیزی رفته باشد: اگر هنوز داخل پنجره عقب‌نشینی باشیم، بدون هیچ درخواستی برمی‌گردد. فراخوانی‌های هم‌زمان زنجیر می‌شوند نه ادغام؛ رویدادی که بعد از خالی‌شدن صف و پیش از settle شدن آن پاس اضافه شود، وگرنه «تحویل‌شده» گزارش می‌شد در حالی که هنوز روی دیسک است.

stats() این شکل را برمی‌گرداند:

TypeScript
{
  queued: number;          // چند پیام همین حالا در صف است
  sent: number;            // چند پیام از init تا حالا پذیرفته شده
  dropped: number;         // چند پیام دور ریخته شده
  failures: number;        // شمار شکست‌های پشت‌سرهم فعلی
  optedOut: boolean;
  durableStorage: boolean; // false یعنی حافظه موقت، صف از reload جان به در نمی‌برد
  anonymousId: string;
  userId: string | null;
}

#گلوبال در برابر نمونه

دو سطح وجود دارد و یکی نیستند.

window.Segmentic فضای نام ماژول است. هفده متد بالا رویش هست، به‌علاوه SegmenticClient، pushSupported، decodeVapidKey، ارزیاب تارگتینگ درون‌سایت (eligible، matches، maySee، isLive، deviceOf، readSeen، writeSeen، recordSeen، recordAction) و کلید default.

نمونه‌ای که init() برمی‌گرداند سه متد دارد که روی گلوبال نیستند:

TypeScript
close(): void                          // تایمرها و شنونده‌ها را متوقف می‌کند
onsiteCampaigns(): OnsiteCampaign[]     // آخرین فهرست کمپین‌های گرفته‌شده
refreshOnsite(): void                   // دوباره تصمیم بگیر کدام کمپین را نشان بدهی

برای رسیدن به این سه تا باید خروجی init() را نگه دارید:

JavaScript
const segmentic = Segmentic.init({
  writeKey: "wk_seg_...",
  apiHost: "https://in.segmentic.net"
});

// بعد از هر تغییر مسیر در یک اپ تک‌صفحه‌ای
router.afterEach(() => {
  segmentic.page();
  segmentic.refreshOnsite();
});

#چیزی که روی سیم می‌رود

هر پیام این شکل را دارد. فیلدهای خالی حذف می‌شوند، null فرستاده نمی‌شود.

TypeScript
{
  type: "track" | "identify" | "page" | "screen" | "alias";
  message_id: string;      // همیشه هست
  timestamp: string;       // ISO 8601، همیشه هست
  sent_at?: string;        // لحظه ارسال، در زمان ارسال مهر می‌خورد
  event?: string;
  user_id?: string;
  anonymous_id?: string;
  previous_id?: string;
  properties?: Properties;
  traits?: Traits;
  context?: Context;
}

بدنه‌ای که به POST {apiHost}/v1/batch می‌رود، پیام‌ها را داخل batch می‌گذارد و sent_at را هم روی پاکت و هم روی تک‌تک پیام‌ها می‌نویسد:

Shell
curl -X POST https://in.segmentic.net/v1/batch \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "sent_at": "2026-07-30T12:00:00.000Z",
    "batch": [
      {
        "type": "track",
        "message_id": "0f9c6f1e-3d4a-4a1e-9c5b-2b7a1f6d8e30",
        "timestamp": "2026-07-30T12:00:00.000Z",
        "sent_at": "2026-07-30T12:00:00.000Z",
        "anonymous_id": "6b7d2c11-8a45-4f0e-9d33-1c2e5b8a7f44",
        "event": "order_completed",
        "properties": { "revenue": 2500000, "currency": "IRR", "city": "تهران" },
        "context": {
          "library": { "name": "segmentic-js", "version": "0.1.0" },
          "session_id": "2a3c1d55-77b0-4c8e-9a1f-0e6d4b3a2c19",
          "locale": "fa-IR",
          "timezone": "Asia/Tehran",
          "page": {
            "url": "https://shop.example.com/checkout/done",
            "path": "/checkout/done",
            "search": "",
            "title": "سفارش ثبت شد",
            "referrer": "https://shop.example.com/cart"
          }
        }
      }
    ]
  }'

پاسخ بسته‌ای که تمامش پذیرفته شده:

JSON
{ "status": "ok", "accepted": 1 }

پاسخ وقتی یک قلم از سه قلم بد است. بقیه پذیرفته می‌شوند و کالکتور می‌گوید کدام ایندکس رد شده است:

JSON
{
  "status": "ok",
  "accepted": 2,
  "rejected": 1,
  "errors": [{ "index": 1, "reason": "missing_identity" }]
}

هدرها: Content-Type: application/json و Authorization: Bearer {writeKey}. پرچم keepalive فقط وقتی روشن می‌شود که طول بدنه کمتر از ۶۰۰۰۰ بایت باشد.

سقف‌های سمت سرور: حداکثر ۵۰۰ قلم در هر بسته و حداکثر پنج مگابایت بدنه. با پیش‌فرض batchSize برابر ۲۰ به هیچ‌کدام نزدیک نمی‌شوید.

#کانتکست خودکار

با autoContext: true این‌ها روی هر پیام می‌روند:

  • library با نام و نسخه کتابخانه. همیشه، و قابل بازنویسی نیست: کانتکستی که خودتان پاس می‌دهید روی بقیه فیلدها می‌نشیند ولی library به مال ما برمی‌گردد
  • session_id، همیشه
  • locale از navigator.language
  • timezone از Intl.DateTimeFormat().resolvedOptions().timeZone، داخل try و catch، چون Intl روی بعضی مرورگرهای توکار نیست
  • screen با width، height و density
  • page با url، path، search، title و referrer
  • network با cellular و wifi، فقط وقتی navigator.connection.type وجود داشته باشد

device و os از قصد فرستاده نمی‌شوند. کالکتور آن‌ها را از هدر User-Agent درمی‌آورد که کلاینت نمی‌تواند جعلش کند؛ هرچه از اینجا برود یک حدس است نه یک واقعیت. IP هم از خود اتصال برداشته می‌شود، نه از بدنه.

با autoContext: false فقط library و session_id می‌روند. انتساب کمپین در این حالت هم می‌رود، چون جوابی است که خود مشتری خواسته، نه چیزی که ما درباره بازدیدکننده جمع کرده‌ایم.

کانتکست هر فراخوانی روی کانتکست جمع‌شده می‌نشیند و زیرشاخه‌های app، page و campaign سطحی ادغام می‌شوند:

JavaScript
Segmentic.track("video_played", { id: 42 }, {
  app: { name: "shop-web", version: "5.2.1" }
});

#انتساب کمپین

این پارامترهای آدرس خوانده می‌شوند:

پارامتر آدرسمی‌رود در context.campaign.
utm_sourcesource
utm_mediummedium
utm_campaignname
utm_termterm
utm_contentcontent
sg_midmessage_id
sg_ttoken
sg_cidcampaign_id، فقط اگر عدد متناهی و بزرگ‌تر از صفر باشد

انتساب فقط با sg_mid شروع می‌شود. آدرسی که فقط UTM دارد هیچ کلیکی ثبت نمی‌کند.

وقتی sg_mid تازه‌ای دیده شود، SDK یک پیام track با نام رویداد message_clicked در صف می‌گذارد، یک بار، تا وقتی شناسه پیام دیگری جایش را بگیرد. رفرش، دکمه بازگشت، یا لینکی که بین همکارها دست به دست شده، کلیک دوم حساب نمی‌شود. مقایسه با همان یک شناسه داخل حافظه است نه با تاریخچه‌ای از شناسه‌ها، پس بازدیدکننده‌ای که روی یک پیام بیاید، بعد روی پیام دوم، و بعد دوباره روی همان پیام اول، کلیک پیام اول را دو بار گزارش می‌کند.

کمپین گرفته‌شده در localStorage با یک زمان ذخیره می‌شود و روی هر رویداد بعدی تا هفت روز پخش می‌شود. هفت روز درست برابر پنجره سرور است؛ دو پنجره متفاوت یعنی SDK تبدیل‌هایی بفرستد که سرور بی‌صدا دور می‌ریزد.

چون از حافظه پخش می‌شود نه از نوار آدرس، تمیزکردن آدرس با replaceState انتساب را از بین نمی‌برد. اگر خواندن مستقیم از query انجام می‌شد، در یک اپ تک‌صفحه‌ای پارامترها می‌ماندند و کل بازدید به آن پیام نسبت داده می‌شد، و در سایتی که آدرسش را تمیز می‌کند اعتبار یک کلیک بعد از بین می‌رفت. هر دو غلط‌اند و هیچ‌کدام در تست دیده نمی‌شود.

یک sg_mid تازه‌تر جای قبلی را می‌گیرد و کلیک دوم را گزارش می‌کند. reset() انتساب را می‌اندازد. بازدیدکننده‌ای که انصراف داده هیچ‌چیز ثبت نمی‌کند، حتی کلیک کمپین.

SDK هرگز به sg_t نگاه نمی‌کند. شناسه پیام مشتق‌شده و حدس‌زدنی است و آن توکن چیزی است که کلیک واقعی را از چیزی که هر کسی می‌تواند در نوار آدرس تایپ کند جدا می‌کند؛ کار SDK فقط برگرداندن آن است.

#نشست

یک session_id به ازای هر بازدید. شناسه بعد از sessionTimeout بی‌کاری عوض می‌شود، نه با بارگذاری صفحه، تا کسی که ده دقیقه مقاله می‌خواند و بعد کلیک می‌کند در همان نشست بماند. نشست منقضی‌شده در بارگذاری بعدی زنده نمی‌شود.

#صف آفلاین

هر پیام قبل از هر تلاش شبکه‌ای روی localStorage نوشته می‌شود، زیر کلید segmentic_queue. بستن تب وسط درخواست، قطع‌شدن آنتن، یا قطعی سمت ما هیچ هزینه‌ای ندارد.

localStorage با یک نوشتن واقعی (کلید __segmentic_probe__) آزمایش می‌شود، نه با اعتماد به اینکه شیئش وجود دارد. در حالت خصوصی سافاری، مرورگرهای سازمانی سخت‌گیر و وقتی سهمیه مبدأ پر شده، این نوشتن خطا می‌دهد و SDK به یک انبار حافظه‌ای برمی‌گردد که دست‌کم صفحه جاری را کار می‌اندازد. stats().durableStorage می‌گوید کدام یکی فعال است.

ارسال در دو حالت اتفاق می‌افتد: وقتی طول صف به batchSize برسد، و هر flushInterval. رویداد online مرورگر هم یک ارسال راه می‌اندازد. تخلیه حلقه می‌زند و هر بار batchSize قلم برمی‌دارد تا صف خالی شود؛ با batchSize: 2 و پنج رویداد، اندازه بسته‌ها ۲ و ۲ و ۱ می‌شود.

صف خراب هزینه‌ای ندارد. JSON بی‌ریخت باعث می‌شود کلید حذف و صف خالی برگردانده شود، و قلم‌هایی که شکل پیام ندارند (بدون message_id رشته‌ای یا بدون type رشته‌ای) تک‌تک فیلتر می‌شوند نه اینکه کل بافر دور ریخته شود.

#چه چیزی حذف می‌شود و چطور گزارش می‌شود

سه دلیل حذف وجود دارد. همه‌شان در stats().dropped جمع می‌شوند و با debug: true در کنسول به شکل [segmentic] dropped <n> messages: <reason> نوشته می‌شوند.

دلیلچه وقت
queue_fullطول صف از maxQueueSize رد شده است
storage_quotaنوشتن روی دیسک شکست خورد، نیمی از بافر ریخته شد و دوباره تلاش شد
storage_unavailableنوشتن دوم هم شکست خورد. کار در حافظه ادامه پیدا می‌کند و آنچه مانده با بستن تب می‌رود

سرریز از جلوی صف حذف می‌کند، یعنی قدیمی‌ترین‌ها. بعد از یک قطعی طولانی، تازه‌ترین رویدادها آن‌هایی هستند که هنوز ارزش داشتن دارند. با maxQueueSize: 10 و بیست‌وپنج رویداد، ده تای آخر می‌مانند و پانزده تا در dropped شمرده می‌شوند.

یک راه حذف چهارم هم هست که در dropped می‌آید و از صف نمی‌آید: پاسخ 4xx سرور، که پایین‌تر توضیح داده شده.

#تلاش دوباره و کدهای وضعیت

پاسخرفتار
2xxتایید شد، از صف پاک می‌شود، شمارنده شکست صفر می‌شود
4xx به‌جز 429برای همیشه دور ریخته می‌شود. از صف پاک می‌شود، در dropped شمرده می‌شود، و با debug این خط لاگ می‌شود: server rejected batch permanently: <status>
429در صف می‌ماند و دوباره تلاش می‌شود
5xxدر صف می‌ماند، عقب‌نشینی اعمال می‌شود
خطای شبکه، DNS یا CORSگذرا فرض می‌شود، در صف می‌ماند

دلیل رفتار با 4xx: پاسخ 4xx یعنی محتوا غلط است و هرگز پذیرفته نخواهد شد. تلاش بی‌پایان روی آن، هر رویداد بعدی را هم پشتش قفل می‌کند. پس دور ریخته می‌شود، ولی با صدا.

همین منطق سمت سرور هم رعایت شده: وقتی خود جست‌وجوی کلید نوشتن ناموفق باشد، کالکتور 503 با Retry-After: 5 می‌دهد و نه 401. یک SDK پاسخ 401 را «این کلید هرگز کار نخواهد کرد» می‌خواند و رویدادها را دور می‌ریزد؛ 503 را «بعد دوباره امتحان کن» می‌خواند و نگهشان می‌دارد.

عقب‌نشینی نمایی با jitter کامل است: تاخیر یک عدد تصادفی یکنواخت بین صفر و min(300000, 1000 * 2^n) میلی‌ثانیه، که n شمار شکست‌های پشت‌سرهم است. یعنی سقف پنج دقیقه. jitter از خود منحنی مهم‌تر است: وقتی یک بک‌اند برمی‌گردد، هزاران دستگاهی که هم‌زمان شکست خورده‌اند نباید هم‌زمان دوباره تلاش کنند و دوباره بیندازندش.

maxRetries فقط سقف رشد این فاصله است، نه سقف نگهداری داده. وقتی شمار شکست‌ها به آن برسد، این خط لاگ می‌شود: max retries reached; messages stay queued for the next session. پیام‌ها می‌مانند. کاربر ممکن است فقط در قطار باشد.

#یکتاسازی با message_id

message_id یک UUID است که یک بار، موقع اضافه‌شدن به صف، ساخته می‌شود و روی هر تلاش دوباره همان می‌ماند. کل مبنای یکتاسازی همین است: روی شبکه موبایل بی‌کیفیت SDK دوباره می‌فرستد، و بدون شناسه ثابت، شمار خریدهای مشتری بی‌صدا دو برابر می‌شود.

شناسه از crypto.randomUUID() می‌آید، وگرنه از crypto.getRandomValues() با بیت‌های نسخه چهار RFC 4122 که دستی ست می‌شوند، و در آخر برای مرورگرهای خیلی قدیمی از Math.random(). مسیر آخر ضعیف‌تر است، ولی یک شناسه تصادم‌کرده فقط یک رویداد یکتاشده هزینه دارد در حالی که خطا پرتاب‌کردن آنجا همه رویدادها را هزینه می‌کند.

سمت سرور، کالکتور شناسه‌های دیده‌شده را نگه می‌دارد. مدت نگهداری از DEDUPE_TTL می‌آید و پیش‌فرضش ۴۸ ساعت است. تکراری‌ها برای فرستنده «پذیرفته‌شده» شمرده می‌شوند، چون SDK یک بار تحویلشان داده و باید دست از تلاش بردارد.

#تصحیح ساعت دستگاه

هر بسته sent_at را روی پاکت و روی تک‌تک پیام‌ها می‌گذارد. کالکتور اختلاف بین زمان دریافت خودش و آن sent_at را حساب می‌کند و همان اختلاف را روی timestamp رویداد اعمال می‌کند. ساعتی که غلط ولی یکنواخت است، این‌طوری بازیابی می‌شود.

جزئیاتی که اهمیت دارند:

  • اختلاف کمتر از یک دقیقه نادیده گرفته می‌شود
  • تصحیح فقط وقتی اعمال می‌شود که زمان اصلاح‌شده داخل پنجره مجاز بماند؛ وگرنه زمان اصلی نگه داشته می‌شود
  • زمانی که بیش از یک ساعت جلوتر از سرور باشد به زمان دریافت گیره می‌شود و هشدار timestamp_in_future برمی‌گردد
  • زمانی که از پنجره گذشته عقب‌تر باشد به لبه آن پنجره گیره می‌شود و هشدار timestamp_too_old برمی‌گردد. پنجره همان نگهداشت رویداد خود حساب است: حسابی که رویدادها را برای همیشه نگه می‌دارد ۳۶۵۰ روز می‌گیرد، حسابی که ۹۰ روز تنظیم کرده ۹۰ روز می‌گیرد، و حسابی که روی کف ۳۰ روزه نشسته باز هم همان پیش‌فرض کامل ۳۰ روزه را دارد

#هنگام بستن صفحه

SDK به visibilitychange و pagehide گوش می‌دهد و در هر دو یک بسته را با navigator.sendBeacon می‌فرستد، به شکل یک Blob از نوع application/json به این آدرس:

{apiHost}/v1/batch?write_key={writeKey}

کلید نوشتن اینجا در query string می‌رود چون sendBeacon نمی‌تواند هدر ست کند و کالکتور درست برای همین حالت آن را آنجا هم می‌پذیرد.

پیام‌های beacon شده از صف پاک نمی‌شوند. sendBeacon فقط گزارش می‌دهد که درخواست به مرورگر تحویل داده شد، هرگز نمی‌گوید رسید. ماندنشان یعنی بارگذاری بعدی ممکن است دوباره بفرستد، که بی‌خطر است، چون هر پیام message_id ثابت دارد.

#کلیدهای ذخیره‌سازی

کلیدمحتوا
segmentic_anonymous_idشناسه ناشناس این مرورگر
segmentic_user_idآخرین شناسه‌ای که به identify() داده شده
segmentic_queueصف خروجی
segmentic_sessionشناسه نشست، زمان شروع و زمان آخرین فعالیت
segmentic_opt_outمقدار 1 یعنی انصراف داده است
segmentic_campaignکمپینی که این بازدید به آن نسبت داده شده، با زمانش
segmentic_traitsویژگی‌های اسکالر آخرین identify()، برای تارگتینگ درون‌سایت
segmentic_seenاین مرورگر پیش‌تر اینجا بوده است. با reset() پاک نمی‌شود
sg_onsiteچند بار هر کمپین درون‌سایت دیده، بسته یا تبدیل شده

به‌علاوه یک ورودی Cache API با نام segmentic-config و کلید /__segmentic_push_config که فقط برای اعلان مرورگر است.

هیچ کوکی‌ای نوشته نمی‌شود.

#رضایت، انصراف و Do Not Track

optOut() جمع‌آوری را متوقف می‌کند و صف را پاک می‌کند. این عمدی است: رعایت انصراف فقط برای رویدادهای آینده، در حالی که چیزی که پیش‌تر گرفته شده بی‌سروصدا تحویل داده شود، انصراف نیست.

بعد از optOut():

  • enqueue بی‌درنگ برمی‌گردد، یعنی track، identify، page، screen و alias هیچ‌چیز نمی‌سازند
  • flush() بدون هیچ درخواستی resolve می‌شود
  • beacon هنگام بستن صفحه کاری نمی‌کند
  • کلیک کمپین ثبت نمی‌شود، پس segmentic_campaign هرگز نوشته نمی‌شود
  • subscribeToPush() با { state: "failed", reason: "..." } برمی‌گردد
  • هیچ پیام درون‌سایتی کشیده نمی‌شود

پرچم انصراف در localStorage می‌ماند، پس از بارگذاری دوباره جان سالم به در می‌برد. optIn() پرچم و کلید را پاک می‌کند و تایمر ارسال را دوباره راه می‌اندازد.

یک نکته صادقانه: optOut() فقط تایمر ارسال را متوقف می‌کند. تایمر شصت‌ثانیه‌ای درون‌سایت متوقف نمی‌شود، پس اگر SDK قبل از انصراف شروع به نظرسنجی کرده باشد، همچنان هر شصت ثانیه یک GET /v1/onsite می‌زند. آن درخواست هیچ هویتی حمل نمی‌کند و هیچ چیزی کشیده یا گزارش نمی‌شود، ولی درخواست ادامه دارد. فقط close() روی نمونه، آن را می‌بندد.

Do Not Track با respectDoNotTrack: true (پیش‌فرض) خوانده می‌شود: به ترتیب navigator.doNotTrack، سپس globalThis.doNotTrack، سپس navigator.msDoNotTrack؛ مقدار "1" یا "yes" یعنی انصراف. این فقط یک بار، در سازنده خوانده می‌شود. عوض‌کردن تنظیم مرورگر وسط نشست تا init() بعدی هیچ اثری ندارد. به همین دلیل optIn() در همان نشست حتی زیر Do Not Track هم جمع‌آوری را دوباره روشن می‌کند.

#اعلان مرورگر

دو پیش‌نیاز دارد و هیچ‌کدام اختیاری نیستند.

یک: identify() باید اجرا شده باشد. اشتراک به یک آدم ذخیره می‌شود و اشتراکی که برای بازدیدکننده ناشناس ذخیره شود هرگز با هیچ سگمنتی هدف‌گیری نمی‌شود. بدون آن { state: "failed", reason: "اول باید identify صدا زده شود تا اشتراک به کاربر وصل شود" } برمی‌گردد.

دو: کلید عمومی VAPID. بدون آن { state: "failed", reason: "کلید VAPID تنظیم نشده است" } برمی‌گردد.

کلید عمومی VAPID امروز در هیچ صفحه‌ای از پنل نشان داده نمی‌شود و هیچ اندپوینتی آن را برنمی‌گرداند. یک کلید در سطح کل نصب است که با adminctl vapid ساخته و به شکل VAPID_PUBLIC_KEY در محیط کالکتور و ورکر گذاشته می‌شود. برای گرفتنش با تماس خودتان در سگمنتیک هماهنگ کنید. عوض‌شدن این کلید، اشتراک هر مرورگری که پیش‌تر ثبت شده را باطل می‌کند.

اشتراک گرفتن با رسیدن یکی نیست، و امروز این شکاف واقعی است. مسیر تحویل پیش از آنکه به فرستنده اعلان مرورگر برسد، از دفتر دستگاه‌ها می‌پرسد این user_id چند نصب دارد، و کانال webpush را دقیقا مثل پوش موبایل حساب می‌کند: نشانی یک نصب، نه نشانی یک آدم. اگر آن کاربر هیچ ردیفی در دفتر دستگاه‌ها نداشته باشد، پیام پیش از هر تلاشی با دلیل not_reachable کنار گذاشته می‌شود. این SDK فقط POST /v1/webpush/subscribe را صدا می‌زند و هیچ‌وقت POST /v1/devices را، پس بازدیدکننده‌ای که فقط مرورگر دارد و اپی نصب نکرده، اشتراکش ذخیره می‌شود و هیچ کمپینی به او نمی‌رسد. در گزارش هم به شکل «شکست» دیده نمی‌شود، به شکل «قابل دسترسی نبود». پیش از اینکه اعلان مرورگر را روی یک سایت بدون اپ راه بیندازید، این را با تماس خودتان در سگمنتیک بررسی کنید.

#سرویس ورکر

فایل segmentic-sw.js را باید از ریشه مبدأ خودتان سرو کنید. دامنه یک ورکر نمی‌تواند از مسیری که از آن سرو شده گسترده‌تر باشد، پس ورکری که از /static/ بیاید فقط برای صفحه‌های زیر /static/ پوش می‌گیرد. مبدأ ما هم به کار نمی‌آید: سرویس ورکر باید هم‌مبدأ با صفحه باشد.

نسخه جاری فایل از پنل قابل دانلود است:

Shell
curl -o segmentic-sw.js https://app.segmentic.net/segmentic-sw.js

آن را کنار index.html خودتان بگذارید تا روی https://your-site.example/segmentic-sw.js سرو شود. اگر جای دیگری گذاشتید، serviceWorkerPath و scope را متناسبش بدهید.

کاری که ورکر می‌کند: در install بلافاصله skipWaiting و در activate بلافاصله clients.claim می‌زند، پس نسخه تازه بدون بسته‌شدن همه تب‌ها تحویل کار را می‌گیرد. اعلان‌ها با dir: "rtl" و lang: "fa" کشیده می‌شوند و badge به icon برمی‌گردد. پوشی که JSON ما نباشد باز هم یک اعلان با عنوان پیام جدید نشان می‌دهد، چون هیچ مرورگری پوش خاموش را اجازه نمی‌دهد و کروم به‌جایش اعلان «این سایت در پس‌زمینه به‌روز شد» خودش را می‌گذارد که بدتر است.

کلیک روی اعلان، اگر تبی با همان آدرس باز باشد همان را فوکوس می‌کند، وگرنه پنجره تازه باز می‌کند. شناسه‌ها (sg_mid و توکن امضاشده) روی خود آدرس سوارند، پس کلیک را سایت خودتان موقع بارگذاری گزارش می‌کند و هیچ سرویس ریدایرکتی از ما وسط نیست. همین است که وقتی آنالیتیکس ما کار نکند، لینک همچنان کار می‌کند.

ورکر به pushsubscriptionchange هم گوش می‌دهد و اشتراک تازه را دوباره پست می‌کند. برای این کار به apiHost، کلید نوشتن، کلید عمومی و شناسه کاربر نیاز دارد و آن‌ها را موقع اشتراک در Cache API ذخیره می‌کنیم، چون این رویداد ممکن است بدون هیچ تب بازی اجرا شود و ورکر به متغیرهای صفحه دسترسی ندارد. بدون این، سرویس پوش می‌تواند اشتراک را خودش بچرخاند و آن آدم بی‌صدا از هر کمپینی بیفتد بیرون؛ تنها نشانه‌اش نرخ تحویلی است که در طول ماه‌ها آرام پایین می‌رود.

#متدهای پوش

TypeScript
subscribeToPush(options: {
  publicKey: string;             // اجباری
  serviceWorkerPath?: string;    // پیش‌فرض "/segmentic-sw.js"
  scope?: string;                // پیش‌فرض "/"
}): Promise<{ state: PushState; reason?: string }>

unsubscribeFromPush(): Promise<boolean>
pushPermission(): NotificationPermission | "unsupported"

PushState یکی از subscribed، denied، dismissed، unsupported یا failed است. reason یک رشته فارسی است که می‌شود به بازدیدکننده نشان داد یا لاگ کرد.

subscribeToPush را از داخل یک کلیک صدا بزنید. مرورگر فقط یک بار به یک سایت اجازه پرسیدن می‌دهد، و کروم اگر به‌اندازه کافی آدم پنجره را ببندند، پرسیدن آن سایت را برای همیشه می‌بندد. پس درخواست بی‌مقدمه هنگام بارگذاری صفحه، مطمئن‌ترین راه از دست دادن دائمی این کانال است. SDK این را اجبار نمی‌کند و جلوی شما را نمی‌گیرد؛ فقط عواقبش برگشت‌ناپذیر است.

HTML
<button id="notify">اعلان تخفیف‌ها را روشن کن</button>

<script src="https://in.segmentic.net/sdk/segmentic.js"></script>
<script>
  Segmentic.init({
    writeKey: "wk_seg_...",
    apiHost: "https://in.segmentic.net"
  });
  Segmentic.identify("u_123", { city: "تهران" });

  document.getElementById("notify").addEventListener("click", async () => {
    const result = await Segmentic.subscribeToPush({
      publicKey: "BEl62iUYgUivxIkv69yViEuiBIa40HI0DLLuxazjqAKeFXlyeeVpMS0"
    });
    if (result.state !== "subscribed") {
      console.log(result.state, result.reason);
    }
  });
</script>

ترتیب بررسی‌ها، و پاسخ هرکدام:

حالتstatereason
کاربر انصراف دادهfailedکاربر از ردیابی انصراف داده است
identify() صدا زده نشدهfailedاول باید identify صدا زده شود تا اشتراک به کاربر وصل شود
مرورگر یکی از سه API را نداردunsupportedاین مرورگر از اعلان وب پشتیبانی نمی‌کند
publicKey خالیfailedکلید VAPID تنظیم نشده است
اجازه پیش‌تر رد شدهdeniedکاربر قبلاً اجازهٔ اعلان را رد کرده است
پنجره اجازه با رد بسته شدdeniedاجازهٔ اعلان داده نشد
پنجره اجازه بدون جواب بسته شدdismissedپنجرهٔ اجازه بسته شد
ثبت روی سرور شکست خوردfailedثبت اشتراک روی سرور انجام نشد
موفقsubscribedندارد

سه نکته در این جدول:

اگر Notification.permission از قبل denied باشد، SDK دیگر requestPermission را صدا نمی‌زند. پرسیدن دوباره از کسی که پیش‌تر نه گفته، هم بی‌فایده است و هم در کروم یک قدم به سمت بسته‌شدن دائمی پنجره‌های آن سایت.

dismissed از denied جداست چون معنی‌اش فرق دارد. کسی که پنجره را بست، رد نکرده است؛ سایت می‌تواند بعد دوباره بپرسد، و یک denied سفت این را به اشتباه منتفی می‌کرد.

اگر اشتراکی از قبل روی این ثبت وجود داشته باشد، همان دوباره استفاده می‌شود نه اینکه یکی تازه ساخته شود. لغو و ثبت دوباره یک endpoint تازه می‌سازد و ردیف قدیمی در جدول به اشتراکی اشاره می‌کند که مرورگر فراموشش کرده؛ بعد از آن هر کمپین به ازای هر endpoint کهنه یک شکست گزارش می‌دهد.

سه بررسی دیگر که SDK می‌کند و در جدول نیست: pushSupported() هر سه تای serviceWorker در navigator، PushManager در window و Notification در window را می‌خواهد؛ بعد از ثبت ورکر، navigator.serviceWorker.ready انتظار کشیده می‌شود، چون صدا زدن pushManager روی ثبتی که هنوز در حال نصب است روی سافاری خطا می‌دهد؛ و اشتراک همیشه با userVisibleOnly: true گرفته می‌شود، چون هر مرورگری که Push API را پیاده کرده این را اجباری می‌داند.

بعد از موفقیت، این درخواست مستقیم فرستاده می‌شود، نه از راه صف:

Shell
curl -X POST https://in.segmentic.net/v1/webpush/subscribe \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "u_123",
    "subscription": {
      "endpoint": "https://fcm.googleapis.com/fcm/send/abc123",
      "p256dh": "BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQtUbVlUls0VJXg7A8u-Ts1XbjhazAkj7I99e8QcYP7DkM",
      "auth": "tBHItJI5svbpez7KI4CCXg"
    }
  }'
JSON
{ "status": "ok" }

از راه صف نمی‌رود چون هیچ بافری نمی‌تواند این را دوباره پخش کند: اجازه گرفته شده و مرورگر دوباره نمی‌پرسد، پس اشتراکی که به ما نرسد یعنی آدمی که راضی شده اعلان بگیرد و هرگز نمی‌شود به او رسید. به همین دلیل پاسخ غیر 2xx به شکل { state: "failed" } به فراخوان برگردانده می‌شود تا صفحه میزبان بتواند دوباره تلاش کند.

unsubscribeFromPush() اشتراک مرورگر را می‌خواند، unsubscribe() می‌زند و بعد POST {apiHost}/v1/webpush/unsubscribe با بدنه { "endpoint": "..." } می‌فرستد. اگر اشتراکی نبود، false برمی‌گرداند بدون هیچ درخواستی. سرور هیچ شناسه کاربری نمی‌خواهد و هیچی چک نمی‌کند: خود endpoint راز آن اشتراک است و داشتنش از قبل برای فرستادن به آن مرورگر کافی است، پس سخت‌گیری برای قطع کردن، محافظت از جهت اشتباه است.

pushPermission() مقدار Notification.permission را بدون پرسیدن برمی‌گرداند، یا "unsupported" وقتی هر سه API موجود نباشند.

#پیام‌های درون‌سایت

با onsite: true (پیش‌فرض) SDK فهرست کمپین‌های زنده را می‌گیرد و در مرورگر تصمیم می‌گیرد کدام را نشان بدهد.

GET {apiHost}/v1/onsite?write_key={writeKey}

یک بار موقع init() و بعد هر شصت ثانیه. شصت ثانیه درست برابر Cache-Control پاسخ است؛ گرفتن سریع‌تر کش را از دست می‌دهد و یک درخواست روی بارگذاری صفحه مشتری می‌گذارد برای جوابی که ممکن نیست عوض شده باشد. پاسخ هیچ هویتی حمل نمی‌کند، پس برای همه بازدیدکننده‌ها یکی است و قابل کش‌شدن.

شکست این درخواست هیچ صدایی ندارد. این کد داخل بارگذاری صفحه یک نفر دیگر اجرا می‌شود؛ خرابی ما باید به «امروز بنری نیست» تنزل کند، نه به یک خطای قرمز در کنسول سایت آن‌ها. سمت سرور هم همین‌طور: خطای پایگاه‌داده به‌جای 5xx یک فهرست خالی برمی‌گرداند.

تارگتینگ محلی است چون جایگزینش یک درخواست به ازای هر بازدید صفحه است، برای یک فروشگاه متوسط ایرانی روزی یک میلیون درخواست، روی مسیر بحرانی رندر سایتشان، با تاخیر ما جلوی محتوایشان و دسترس‌پذیری ما جلوی کسب‌وکارشان. هزینه این تصمیم این است که قواعد تارگتینگ عمومی‌اند: هر کسی می‌تواند در تب شبکه بخواندشان، و به همین دلیل واژگان قاعده از قصد چیزی ندارد که مشتری از دیدنش توسط رقیب ناراحت شود.

قواعدی که در مرورگر ارزیابی می‌شوند: url_contains، url_not_contains، devices، new_visitors_only، returning_only، logged_in و traits.

تطبیق آدرس زیررشته‌ای است، نه regex. الگویی که یک بازاریاب می‌نویسد الگویی است که می‌تواند فاجعه‌بار کند باشد، و این روی هر صفحه سایت یکی دیگر می‌دود.

نوع دستگاه از عرض viewport می‌آید نه از User-Agent: کمتر از ۷۶۸ یعنی mobile، کمتر از ۱۰۲۴ یعنی tablet، بقیه desktop. تشخیص از User-Agent روی هر دستگاهی که درباره خودش دروغ می‌گوید غلط است، که امروز بیشترشان‌اند، و کمپینی که «موبایل» را هدف می‌گیرد در عمل یعنی «صفحه باریک».

traits برابری ساده با ویژگی‌هایی است که آخرین identify() در همین مرورگر گذاشته، نه با انبار داده. مرورگر فقط چیزی را دارد که به آن داده شده و وانمود به خلافش، هر قاعده ویژگی را بی‌صدا غلط می‌کرد.

سقف تکرار در همین ترتیب اعمال می‌شود:

  1. کمپین خارج از بازه starts_at و ends_at باشد: نه. ends_at بازه را نمی‌گیرد، یعنی لحظه پایان دیگر زنده نیست
  2. تا حالا دیده نشده: بله
  3. converted ثبت شده: نه، برای همیشه. کسی که کار را انجام داده نباید دوباره پرسیده شود، و این از هر قاعده دیگری، حتی از زنده‌بودن کمپین، بالاتر است
  4. dismissed ثبت شده و کمپین dismissible است: نه. بنر غیرقابل‌بستن باز هم نشان داده می‌شود
  5. max_impressions بزرگ‌تر از صفر است و شمار دیده‌شدن به آن رسیده: نه. صفر یعنی بی‌سقف
  6. cooldown_hours بزرگ‌تر از صفر است و از آخرین دیدن کمتر از آن گذشته: نه

هم مرورگر و هم سرور این سقف را اعمال می‌کنند. فقط حافظه محلی یعنی پاک‌کردنش مودالی بی‌سقف می‌دهد؛ فقط سرور یعنی یک درخواست به ازای هر بازدید صفحه، که کل این طراحی برای نبودنش وجود دارد.

یک کلیک برای حساب سقف، تبدیل شمرده می‌شود: کسی که لینک را دنبال کرده کار را انجام داده و نشان‌دادن دوباره یعنی خواستن دو بار انجامش.

#چه چیزی کشیده می‌شود

هر چهار نوع کشیده می‌شوند: banner (نوار بالا یا پایین)، modal (وسط، با پرده پشتش)، slidein (گوشه) و survey (گوشه، با NPS صفر تا ده یا فهرست گزینه).

در هر لحظه فقط یک کمپین. دو مودال هم‌زمان طراحی هیچ‌کس نیست و دومی دکمه بستن اولی را می‌پوشاند. اولین کمپین واجد شرایط، به ترتیبی که سرور فرستاده، انتخاب می‌شود.

سه قاعده کل کد رندر را شکل می‌دهند و هر سه درباره مهمان‌بودن روی صفحه یک نفر دیگرند:

  • هرگز خطا پرتاب نکن. یک ویجت آنالیتیکس نباید چیزی باشد که پرداخت را می‌شکند. هر نقطه ورود در try پیچیده شده است
  • هرگز ارث نبر. ریست CSS، فونت و قاعده‌های * { } صفحه میزبان وگرنه ویجت را جوری بازشکل می‌دادند که هیچ‌کس پیش‌نمایشش را ندیده. هر ویژگی مهم مستقیم روی خود عنصر ست می‌شود
  • هرگز مارک‌آپ تزریق نکن. محتوا با textContent نوشته می‌شود، هرگز با innerHTML. تیتر از یک فیلد پنل می‌آید و مشتری‌ای که مارک‌آپ داخلش پیست کند نباید اسکریپتی روی سایت خودش اجرا شود

آدرس دکمه فقط وقتی href می‌شود که با http:// یا https:// شروع شود یا با / آغاز شود. آدرس javascript: هیچ href نمی‌گیرد. متن دکمه سر جایش می‌ماند و کلیک همچنان گزارش می‌شود.

جزئیات چیدمان: ظرف یک div با position: fixed و شناسه segmentic-onsite است، z-index برابر 2147483000 (زیر بیشینه، تا اورلی خود مشتری بتواند برنده شود)، pointer-events: none روی ظرف با هر ویجت که کلیک را برای خودش برمی‌گرداند (وگرنه یک div نامرئی تمام‌صفحه هر کلیک روی سایت مشتری را می‌بلعید)، و direction: rtl روی ظرف، چون ویجتی که فارسی نوشته شده باید راست‌به‌چپ خوانده شود حتی روی صفحه‌ای که نیست. دکمه بستن سمت چپ است و aria-label آن بستن. رنگ‌های پیش‌فرض: پس‌زمینه #1f2937، متن #ffffff، رنگ تاکید #2563eb.

ماشه‌ها: اسکرول، قصد خروج، و تاخیر. تاخیر فقط وقتی خودش ماشه است که نه اسکرول ست شده باشد و نه قصد خروج؛ کنار آن دو با آن‌ها مسابقه می‌داد و پیام را روی تایمری نشان می‌داد که بازاریاب به‌عنوان حداقل نوشته بود. قصد خروج فقط دسکتاپ است: دستگاه لمسی اشاره‌گری ندارد که به سمت نوار تب برود، و کتابخانه‌هایی که این را جعل می‌کنند روی هر اسکرول به بالا شلیک می‌شوند.

نظرسنجی: سوال، بعد یا ردیف NPS از صفر تا ده که داخل کارت راست‌به‌چپ با direction: ltr کشیده می‌شود (صفر چپ تا ده راست، همان‌طور که این مقیاس همه‌جا کشیده می‌شود) یا فهرست گزینه‌ها. اگر follow_up ست باشد بعد از جواب یک textarea نشان داده می‌شود و متنش به‌عنوان پاسخ دوم می‌رود. آن پاسخ دوم کل جواب را با خودش می‌برد، یعنی نمره یا گزینه را هم کنار متن تازه، چون ذخیره پشتش روی (کمپین، شخص) idempotent است و ردیف را جایگزین می‌کند نه اینکه به آن اضافه کند. تشکر پیش‌فرض ممنون از وقتی که گذاشتید. است و دو ثانیه سر جایش می‌ماند قبل از بسته‌شدن: ویجتی که همان لحظه جواب‌دادن ناپدید شود مثل یک گلیچ صفحه خوانده می‌شود نه مثل یک تایید.

برداشت (impression) قبل از فرستادن گزارش، محلی ثبت می‌شود، تا سقف حتی وقتی درخواست هرگز نرسد سر جایش باشد.

گزارش‌ها مستقیم می‌روند نه از راه صف، با keepalive: true، و خطایشان بلعیده می‌شود. یک برداشت که ده ثانیه دیر برسد اشکالی ندارد، ولی نباید پشت بسته‌ای بماند که منتظر نوزده پیام دیگر است روی صفحه‌ای که بازدیدکننده دارد ترکش می‌کند.

Shell
curl -X POST https://in.segmentic.net/v1/onsite/event \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_id": 7,
    "action": "impression",
    "anonymous_id": "6b7d2c11-8a45-4f0e-9d33-1c2e5b8a7f44",
    "user_id": "u_123"
  }'

action یکی از impression، click، dismiss یا convert است. user_id وقتی بازدیدکننده ناشناس است حذف می‌شود.

پاسخ نظرسنجی جدا می‌رود. سرور فقط دو فیلد جواب می‌خواند و نه چیز دیگری: score که روی نظرسنجی NPS عددی از صفر تا ده است، و answers که نگاشتی از رشته به رشته است و جواب چندگزینه‌ای و متن آزاد جایشان همان‌جاست. هر کلید دیگری موقع decode شدن بدنه دور ریخته می‌شود و پاسخ باز هم 200 است، پس اگر نظرسنجی را خودتان می‌کشید فقط answers بفرستید. رندرکننده ما داخل آن نگاشت از کلیدهای choice و text استفاده می‌کند؛ پنل این دو تا را با برچسب خوانا نشان می‌دهد و هر کلید دلخواه شما را همان‌طور که فرستاده‌اید.

نفرستادن score روی نظرسنجی NPS با 400 رد می‌شود، نه اینکه صفر خوانده شود. این عمدی است و نیمه دوم یک باگ همین SDK بود: نبودن عدد یعنی نبودن عدد، نه بدترین عدد مقیاس.

هر دوی این‌ها تا همین اواخر غلط بودند و جواب‌هایی که در آن فاصله از دست رفته‌اند برگشتنی نیستند. پیام‌های درون‌سایت می‌گوید چه چیزی را باید بررسی کنید.

Shell
curl -X POST https://in.segmentic.net/v1/onsite/response \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_id": 7,
    "anonymous_id": "6b7d2c11-8a45-4f0e-9d33-1c2e5b8a7f44",
    "user_id": "u_123",
    "score": 9,
    "answers": { "reason": "ارسال سریع بود" }
  }'

#رندر با کد خودتان

اگر فروشگاه شما دیزاین‌سیستم خودش را دارد، رندر ما را خاموش کنید و منطق واجدشرایط‌بودن و سقف تکرار را نگه دارید. ارزیاب تارگتینگ از قصد export شده، چون همان بخشی است که ممکن است مشتری بخواهد خودش اجرا کند.

HTML
<script src="https://in.segmentic.net/sdk/segmentic.js"></script>
<script>
  const segmentic = Segmentic.init({
    writeKey: "wk_seg_...",
    apiHost: "https://in.segmentic.net",
    onsite: false
  });

  // فهرست کمپین‌ها با onsite: false گرفته نمی‌شود، پس خودتان بگیرید.
  fetch("https://in.segmentic.net/v1/onsite?write_key=wk_seg_...")
    .then((res) => res.json())
    .then((body) => {
      const seen = Segmentic.readSeen(localStorage);
      const visitor = {
        url: location.href,
        device: Segmentic.deviceOf(window.innerWidth),
        loggedIn: segmentic.getUserId() !== null,
        returning: localStorage.getItem("segmentic_seen") === "1",
        traits: JSON.parse(localStorage.getItem("segmentic_traits") || "{}"),
        now: Date.now()
      };
      const list = Segmentic.eligible(body.campaigns || [], visitor, seen);
      if (list[0]) {
        myOwnBanner(list[0]);
        Segmentic.writeSeen(
          localStorage,
          Segmentic.recordSeen(seen, list[0].id, visitor.now)
        );
      }
    });
</script>

با onsite: false هیچ فهرستی گرفته نمی‌شود، پس onsiteCampaigns() آرایه خالی می‌دهد و refreshOnsite() بی‌درنگ برمی‌گردد. اگر onsite را روشن بگذارید ولی رندر خودتان را بخواهید، راهی برای خاموش‌کردن فقط رندر وجود ندارد.

#CORS و CSP

هر نقطه‌ای روی کالکتور که کلید نوشتن می‌خواهد، این هدرها را قبل از هر کاری می‌نویسد، حتی قبل از احراز هویت، تا مرورگر کد وضعیت واقعی را ببیند نه یک خطای CORS. این شامل هرچه این SDK صدا می‌زند می‌شود: /v1/batch، /v1/webpush/subscribe و /v1/webpush/unsubscribe، و هر سه نقطه درون‌سایت.

HTTP
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Segmentic-Key
Access-Control-Max-Age: 86400

Access-Control-Allow-Credentials از قصد ست نمی‌شود. مبدأ * فقط به همین دلیل بی‌خطر است.

GET /v1/status هیچ‌کدام از این هدرها را ندارد، پس مرورگر نمی‌تواند آن را از صفحه شما بخواند. آن نقطه برای یک مانیتور یا curl است، نه برای بررسی سلامتی که در فرانت‌اند بزنید. نقطه‌های ایمیلی زیر /e/ هم این هدرها را ندارند.

فهرست متدها GET ندارد و لازم هم نیست: تنها درخواست GET که SDK می‌زند، گرفتن کمپین‌های درون‌سایت است که هیچ هدر سفارشی ندارد، پس درخواستی ساده است و هیچ preflight نمی‌شود.

برای Content-Security-Policy یک ورودی کافی است، چون باندل و رویدادها هر دو روی همان مبدأ‌اند:

script-src https://in.segmentic.net;
connect-src https://in.segmentic.net;

اگر پیام درون‌سایت با image_url منتشر می‌کنید، img-src هم باید مبدأ همان تصویر را داشته باشد. ویجت‌ها استایلشان را inline روی خود عنصر می‌گذارند، نه با تگ style، پس style-src دست نمی‌خورد.

#حجم اندازه‌گیری‌شده

اندازه‌گیری روی فایل ساخته‌شده‌ای که همین حالا سرو می‌شود:

فایلخامبا gzip
باندل تگ اسکریپت (IIFE)۲۶۰۹۱ بایت۸۸۳۲ بایت، همان‌طور که سرو می‌شود
ESM۲۵۵۷۹ بایت۸۵۸۰ بایت با gzip -9 محلی
CJS۲۶۳۸۰ بایت۸۸۶۹ بایت با gzip -9 محلی

عدد gzip یکی نیست و به فشرده‌ساز و سطحش بستگی دارد، پس نباید وانمود کنیم یکی است. ۸۸۳۲ همان چیزی است که in.segmentic.net روی سیم برمی‌گرداند و همان چیزی است که بازدیدکننده دانلود می‌کند؛ همان بایت‌ها با gzip -9 روی لپ‌تاپ ۸۷۹۳ می‌شوند. ESM و CJS را ما میزبانی نمی‌کنیم، پس برایشان فقط عدد محلی هست.

یعنی حدود هشت و نیم کیلوبایت برای نسخه تگ اسکریپت. هیچ وابستگی زمان اجرایی ندارد.

README داخل مخزن تا همین اواخر «۴.۲ کیلوبایت gzip» می‌گفت. آن عدد کهنه بود و حدود نصف عدد واقعی؛ رشد از وقتی است که رندر درون‌سایت و اعلان مرورگر اضافه شدند. خود README حالا آن را در فهرست ادعاهای غلطش آورده و همین ۸۸۳۲ را می‌نویسد. اگر جای دیگری به عدد قدیمی برخوردید، به این صفحه اعتماد کنید.

#عیب‌یابی

debug: true بدهید تا هر کار SDK با پیشوند [segmentic] در کنسول نوشته شود: صف‌شدن هر پیام، ارسال beacon، حذف‌ها با دلیلشان، انتساب کمپین، شکست ارسال با تخمین فاصله تلاش بعدی، و رسیدن به سقف تلاش.

JavaScript
const segmentic = Segmentic.init({
  writeKey: "wk_seg_...",
  apiHost: "https://in.segmentic.net",
  debug: true
});

setInterval(() => console.table(segmentic.stats()), 5000);

سه چیزی که در stats() باید ببینید:

  • durableStorage: false یعنی localStorage در دسترس نبوده و صف با بستن تب می‌رود. حالت خصوصی سافاری یا سهمیه پر
  • dropped که بالا می‌رود در حالی که failures صفر است، یعنی سرور دارد 4xx می‌دهد. با debug کد وضعیت را در کنسول ببینید
  • queued که بالا می‌رود و sent که تکان نمی‌خورد، یعنی هیچ ارسالی موفق نبوده. اول CORS، بعد درست‌بودن apiHost را نگاه کنید

یک چیزی که در پنل نمی‌بینید: دیباگر رویداد زنده نصب این SDK را نشان نمی‌دهد. ضبط دیباگ فقط روی مسیر تک‌رویدادی صدا زده می‌شود و این SDK همیشه /v1/batch می‌فرستد، پس آن صفحه برای یک نصب SDK همیشه خالی است. برای اینکه ببینید رویدادها رسیده‌اند یا نه، صفحه «اتصال» پنل را باز بگذارید؛ آن از فعالیت اپ در بیست‌وچهار ساعت گذشته می‌پرسد و بسته‌ها را می‌بیند.

برای یک بررسی سریع بدون مرورگر:

Shell
curl -X POST https://in.segmentic.net/v1/track \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{"anonymous_id":"test-1","event":"install_check"}'
JSON
{ "status": "ok", "accepted": 1 }

#کارهایی که از قصد انجام نمی‌شود

  • هیچ کوکی‌ای نمی‌نویسد. فقط localStorage و یک ورودی Cache API برای پوش
  • هیچ‌چیز را خودکار شکار نمی‌کند. نه کلیک، نه ارسال فرم، نه خطای جاوااسکریپت، نه ضبط جلسه. فقط چیزی می‌رود که خودتان صدا زده‌اید
  • به تاریخچه مرورگر قلاب نمی‌زند. در اپ تک‌صفحه‌ای، page() را خودتان صدا بزنید
  • device و os نمی‌فرستد. کالکتور از هدر User-Agent درشان می‌آورد، چون کلاینت نمی‌تواند آن را جعل کند
  • fingerprint نمی‌گیرد. هیچ canvas، هیچ فهرست فونت، هیچ شناسه‌ای که از سخت‌افزار مشتق شده باشد
  • توکن کلیک را بازرسی نمی‌کند. sg_t فقط حمل و برگردانده می‌شود
  • در تارگتینگ regex اجرا نمی‌کند. فقط زیررشته
  • innerHTML نمی‌نویسد. هیچ‌جا
  • پیام‌های beacon شده را از صف پاک نمی‌کند. یکتاسازی سمت سرور این را بی‌خطر می‌کند
  • در انصراف، صف را نگه نمی‌دارد. پاکش می‌کند
  • خطا پرتاب نمی‌کند، به‌جز init() بدون writeKey یا بدون apiHost
  • اشتراک اعلان مرورگر را در دفتر دستگاه‌ها ثبت نمی‌کند. فقط POST /v1/webpush/subscribe. بخش اعلان مرورگر می‌گوید چرا این مهم است
  • کلاینت صندوق پیام (inbox) ندارد. اندپوینت‌های POST /v1/inbox و POST /v1/inbox/ack روی کالکتور وجود دارند و تست دارند، ولی هیچ SDKی برایشان کد ندارد. اگر لازمشان دارید، خودتان صدایشان بزنید
  • روی هیچ رجیستری npm منتشر نشده است. بخش با باندلر را ببینید

مرتبط: شروع سریع برای اولین رویداد، هویت برای اینکه تاریخچه ناشناس چطور به حساب وصل می‌شود، رضایت برای سیاست انصراف در کل پلتفرم، پیام‌های درون‌سایت برای ساختن کمپین در پنل، دستگاه‌ها و پوش برای بقیه کانال‌های اعلان، و خطاها و سقف‌ها برای رفتار کالکتور.

قبلیادغام هویتبعدیSDK اندروید

در این صفحه

  • نصب
  • راه‌اندازی و همه گزینه‌ها
  • متدها
  • چیزی که روی سیم می‌رود
  • صف آفلاین
  • رضایت، انصراف و Do Not Track
  • اعلان مرورگر
  • پیام‌های درون‌سایت
  • CORS و CSP
  • حجم اندازه‌گیری‌شده
  • عیب‌یابی
  • کارهایی که از قصد انجام نمی‌شود

سگمنتیک

این صفحه از روی کد نوشته شده است