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

ارسال رویداد از سرور به سرور

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

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

#دو در، و یکی نیستند

کلکتورAPI مدیریت
آدرسhttps://in.segmentic.net/v1/batchhttps://api.segmentic.net/v1/events
کلیدکلید نوشتن، wk_seg_...کلید API، sk_seg_...
دسترسی لازمندارد. خود کلید نوشتن مجوز استprofile.write
نام آرایه در بدنهbatchevents
پاسخ موفق200202
حذف تکراری با message_idداردندارد
برگرداندن هشدارهاداردندارد، دور ریخته می‌شوند
سقف عقب‌بردن زماننگهداشت رویداد همان حسابثابت، ۳۰ روز
context و sent_at در سطح بستهداردچنین فیلدهایی ندارد
بیشترین تعداد آیتم۵۰۰۵۰۰
بیشترین حجم بدنه۵ مگابایت۸ مگابایت
مصرف از بودجهٔ درخواستندارددارد، ۵ واحد از ۶۰۰ در دقیقه

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

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

JSON
{
  "error": {
    "code": "write_key_rejected",
    "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
  }
}

#بک‌اند شما کدام را باید بردارد

POST /v1/batch کلکتور را.

دلیلش حذف تکراری است. خط لولهٔ سفارش شما دوباره خواهد فرستاد. یک تایم‌اوت روی لودبالانسر، یک ریدیپلوی وسط درخواست، یک ورکر که بعد از POST و قبل از علامت‌زدن ردیف کرش می‌کند: هر کدام به یک جا می‌رسند، به فرستادن دوبارهٔ همان بسته. کلکتور message_id را با SET NX در ردیس و پنجرهٔ ۴۸ ساعته ثبت می‌کند، پس تحویل دوم با 200 جواب می‌گیرد و هرگز به انبار داده نمی‌رسد. POST /v1/events این کار را نمی‌کند. پاکت‌ها را مستقیم به صف می‌دهد. یک بستهٔ صد سفارشی که دوباره فرستاده شود می‌شود دویست سفارش، و اولین نشانه‌اش یک عدد درآمد است که هیچ‌کس نمی‌تواند با حسابداری تطبیقش بدهد.

دلیل دوم، پنجرهٔ عقب‌بردن زمان است. کلکتور نگهداشت رویداد همان حساب را می‌خواند و اجازه می‌دهد تا همان‌قدر عقب بروید. POST /v1/events این گزینه را پر نمی‌کند، پس همان پیش‌فرض ثابت ۳۰ روزه اعمال می‌شود و هرچه قدیمی‌تر باشد، بی‌صدا درست به ۳۰ روز قبل منتقل می‌شود. در بخش زمان رویداد توضیح داده شده.

سراغ POST /v1/events بروید وقتی سرویس شما همین حالا یک کلید API دارد و اضافه‌کردن یک کلید دوم به استقرارتان دردسر بزرگ‌تری است، یا وقتی فراخوان‌کننده یک ایجنت است که همین حالا برای سگمنت و گزارش با api.segmentic.net حرف می‌زند. در این حالت بپذیرید که مسئولیت دوباره‌نفرستادن یک رویداد با شماست.

هر دو در به یک خط لوله و یک جدول در انبار داده می‌رسند. هیچ چیزی در پایین‌دست نمی‌تواند بگوید رویداد از کدام در آمده، جز اینکه رویدادهای POST /v1/events مقدار app_id برابر 0 دارند و هیچ آی‌پی و User-Agent همراهشان نیست.

#کلکتور: POST /v1/batch

کلید در Authorization: Bearer می‌آید. دو شکل دیگر هم پذیرفته می‌شود، چون یک بیکن مرورگری نمی‌تواند هدر بگذارد: X-Segmentic-Key: wk_seg_... و ?write_key=wk_seg_.... از سمت سرور همان هدر را بگذارید. فقط رشتهٔ دقیق Bearer (با B بزرگ و یک فاصله) از Authorization جدا می‌شود؛ هر طرح دیگری از این بررسی رد می‌شود، می‌رود سراغ دو شکل بعدی، و در نهایت با «کلید نیست» شکست می‌خورد.

هر آیتم type خودش را دارد. روی آدرس‌های تک‌رویدادی (/v1/track و /v1/identify و بقیه) مسیر تعیین‌کنندهٔ نوع است و type داخل بدنه نادیده گرفته می‌شود، ولی روی /v1/batch مسیری برای خواندن نوع وجود ندارد، پس آیتمی که type نداشته باشد یا مقدار ناشناخته بفرستد رد می‌شود. پنج مقدار مجاز: track، identify، alias، page، screen.

یک بسته با دو رویداد
curl -sS https://in.segmentic.net/v1/batch \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "context": { "locale": "fa-IR" },
    "batch": [
      {
        "type": "track",
        "message_id": "order-8821-completed",
        "event": "order_completed",
        "user_id": "u_44120",
        "timestamp": "2026-08-07T09:12:41Z",
        "properties": {
          "order_id": "8821",
          "revenue": 4800000,
          "currency": "IRR",
          "city": "تهران"
        }
      },
      {
        "type": "identify",
        "message_id": "profile-44120-v7",
        "user_id": "u_44120",
        "traits": { "phone": "09123456789", "city": "تهران" }
      }
    ]
  }'
200 OK
{ "status": "ok", "accepted": 2 }

context و sent_at سطح بسته روی هر آیتمی که مقدار خودش را نداشته باشد کپی می‌شوند، و هرگز روی مقداری که خودش داشته نوشته نمی‌شوند. این برای آن ساخته شده که یک SDK موبایل مدل دستگاه را یک بار برای پنجاه رویداد بفرستد، و از سمت سرور هم برای یک context.campaign مشترک به همان اندازه به‌درد می‌خورد.

فیلدهای هر آیتم:

فیلدنوعاجباریتوضیح
typeرشتهروی batch بلهیکی از track، identify، alias، page، screen
message_idرشتهنه، ولی بفرستیدحداکثر ۲۵۶ بایت. اگر نباشد ساخته می‌شود و هشدار می‌گیرید
eventرشتهوقتی type برابر track است، بلهحداکثر ۱۲۸ بایت بعد از نرمال‌سازی
user_idرشتهیکی از user_id یا anonymous_idحداکثر ۲۵۶ بایت
anonymous_idرشتهیکی از user_id یا anonymous_idحداکثر ۲۵۶ بایت. قاعدهٔ قالب ندارد، لازم نیست UUID باشد
previous_idرشتهوقتی type برابر alias است، بلهشناسه‌ای که از آن ادغام می‌شود. برخلاف بقیهٔ شناسه‌ها هیچ سقف طولی ندارد، فقط همان سقف بدنه
timestampRFC 3339نهپیش‌فرض، زمان دریافت روی سرور
sent_atRFC 3339نهتصحیح اختلاف ساعت را روشن می‌کند. قبل از پرکردنش پایین‌تر را بخوانید
propertiesشیءنهحداکثر ۲۵۶ کلید، هر کلید ۱۲۸ بایت، هر مقدار رشته‌ای ۸۱۹۲ بایت
traitsشیءنهحداکثر ۲۵۶ کلید، با همان حدود
contextشیءنهساختارش در رویدادها آمده

timestamp و sent_at را کتابخانهٔ JSON زبان Go به زمان تبدیل می‌کند و آن فقط RFC 3339 را می‌پذیرد. ثانیهٔ یونیکس، میلی‌ثانیهٔ یونیکس و تاریخ خالی 2026-08-07 هیچ‌کدام تبدیل نمی‌شوند و کل درخواست با 400 malformed JSON رد می‌شود، نه فقط همان یک آیتم.

درآمد از داخل properties خوانده می‌شود، به این ترتیب: اولین مقدار ناصفر از revenue، total، value؛ اگر هیچ‌کدام نبود، price ضرب در quantity که در نبودش ۱ فرض می‌شود. currency پیش‌فرض IRR است و بزرگ‌حرف می‌شود، پس "irt" به شکل IRT ذخیره می‌شود. هیچ تبدیل نرخی انجام نمی‌شود.

هر وضعیتی که این آدرس برمی‌گرداند:

موقعیتکدبدنه
پذیرفته شد، کامل یا بخشی200{"status":"ok","accepted":N,...}
کلیدی روی درخواست نبود401{"status":"error","message":"missing write key"}
کلید ناشناس، باطل‌شده، یا حساب معلق401{"status":"error","message":"invalid write key"}
جست‌وجوی کلید سمت ما شکست خورد503 با Retry-After: 5{"status":"error","message":"cannot verify the write key right now; retry"}
بدنهٔ بزرگ‌تر از ۵ مگابایت413{"status":"error","message":"request body too large"}
بدنه JSON نیست400{"status":"error","message":"malformed JSON"}
batch خالی است400{"status":"error","message":"batch_empty"}
بیشتر از ۵۰۰ آیتم400{"status":"error","message":"batch_too_large: 501 items, limit 500"}
حساب از سقف ماهانه گذشته402{"status":"error","message":"<جملهٔ فارسی>"}
هم باس و هم بافر دیسک شکست خوردند503، بدون Retry-After{"status":"error","message":"temporarily unavailable, please retry"}

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

پیام 402 همیشه فارسی است. کلکتور هیچ میان‌افزار زبانی ندارد، پس Accept-Language: en هیچ اثری روی آن نمی‌گذارد. روی کد وضعیت شرط بگذارید، نه روی متن.

#API مدیریت: POST /v1/events

دو رویداد از راه API مدیریت
curl -sS https://api.segmentic.net/v1/events \
  -H "Authorization: Bearer sk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "type": "track",
        "message_id": "order-8821-completed",
        "event": "order_completed",
        "user_id": "u_44120",
        "timestamp": "2026-08-07T09:12:41Z",
        "properties": { "order_id": "8821", "revenue": 4800000, "currency": "IRR" }
      },
      {
        "type": "track",
        "event": "order_completed",
        "properties": { "order_id": "8822" }
      }
    ]
  }'
202 Accepted
{
  "accepted": 1,
  "rejected": [ { "index": 1, "reason": "missing_identity" } ]
}

202 و نه 200، چون رویدادها در صف نشسته‌اند نه ذخیره‌شده. چند ثانیه بعد قابل کوئری می‌شوند. اگر 200 می‌گفتیم، شما را تشویق می‌کرد بلافاصله بخوانیدشان و نتیجه بگیرید که گم شده‌اند.

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

این مسیر فقط وقتی ثبت می‌شود که استقرار یک importer پیکربندی‌شده داشته باشد. وقتی ندارد، مسیر به هندلر پیش‌فرض می‌افتد و 404 unknown_endpoint می‌گیرد. GET /v1/capabilities وضعیتش را زیر features.ingest گزارش می‌کند.

دسترسی لازم profile.write است که نقش‌های owner و admin و marketer دارند و analyst ندارد. متن رد، خود دسترسی موردنیاز را نام می‌برد تا مجبور نشوید برای فهمیدنش تیکت پشتیبانی باز کنید:

JSON
{
  "error": {
    "code": "forbidden",
    "message": "this key does not carry profile.write, see GET /v1/whoami for what it does carry",
    "need": "profile.write"
  }
}

هر وضعیتی که این آدرس برمی‌گرداند:

موقعیتکدerror.code
پذیرفته شد، کامل یا بخشی202ندارد
کلیدی نبود، یا کلید شناخته نشد401unauthenticated
کلید نوشتن wk_ فرستاده شده401write_key_rejected
تاریخ انقضای کلید گذشته401key_expired
کلید profile.write ندارد403forbidden
events خالی است400batch_empty
بدنه JSON نیست400malformed_json
بیشتر از ۵۰۰ رویداد413batch_too_large، با details برابر {"limit":500,"sent":N}
همهٔ رویدادها رد شدند422all_events_rejected، با آرایهٔ آیتم‌ها در details
حساب از سقف ماهانه گذشته402quota_cancelled، quota_trial_over، quota_event_cap یا quota_message_cap
بودجهٔ درخواست این کلید تمام شده429 با Retry-After: 60budget_exhausted
سرویس بودجه خطا داد503budget_unavailable
صف در دسترس نبود503ingest_unavailable

روی این API فیلد error همیشه یک شیء نیست. ده مسیر از مسیرهای این API هندلر داشبورد را دوباره استفاده می‌کنند و به‌جایش {"error":"یک رشته"} برمی‌گردانند: دو مسیر GET /v1/schema/*، POST /v1/audiences/count، فهرست و خواندن سگمنت، فهرست و خواندن کمپین، دو مسیر POST /v1/reports/*، و POST /v1/messages. خود POST /v1/events همیشه شکل شیء را می‌دهد، ولی کلاینتی که یک پارسر خطا را در کل API به اشتراک می‌گذارد باید قبل از خواندن error.code نوع error را چک کند.

#بسته‌بندی و سقف آن

۵۰۰ آیتم در هر درخواست، روی هر دو در. حد سختی است: ۵۰۱ آیتم کامل رد می‌شود و هیچ‌چیز از آن بسته ذخیره نمی‌شود. متن رد روی API مدیریت خود سقف را داخل details منتشر می‌کند تا کلاینتی که دارد اندازهٔ حلقه‌اش را تنظیم می‌کند، مجبور نباشد عدد را با آزمون و خطا پیدا کند.

سقف حجم بدنه جداست و اول با رویدادهای چاق پر می‌شود نه با تعداد زیاد. ۵ مگابایت روی کلکتور، ۸ مگابایت روی API مدیریت. ۵۰۰ رویداد که هر کدام ۲۵۶ ویژگی داشته باشند خیلی قبل از رسیدن به ۵۰۰ آیتم، از ۵ مگابایت رد می‌شوند.

فشرده‌سازی وجود ندارد. هیچ‌کدام از این دو آدرس Content-Encoding را نمی‌خوانند، پس یک بدنهٔ gzip شده به‌صورت بایت‌هایی می‌رسد که JSON نیستند و با 400 رد می‌شود. اگر رویدادهایتان بزرگ‌اند، درخواست بیشتری بفرستید نه درخواست بزرگ‌تر.

Content-Type هم اجبار نمی‌شود. بدنه هرچه اعلام کنید به‌عنوان JSON خوانده و تجزیه می‌شود. با این حال application/json بفرستید تا پراکسی وسط راه تصمیم دیگری نگیرد.

زیر این سقف، اندازهٔ بسته فقط یک تصمیم توان عملیاتی است و بس. یک بستهٔ ۵۰۰ تایی روی کلکتور فقط یک رفت‌وبرگشت ردیس و یک رفت‌وبرگشت انتشار هزینه دارد، همان‌قدر که یک بستهٔ دوتایی، و دلیل اصلی ارزش داشتن بسته‌بندی همین است.

#شکست بخشی، و خواندن خطای هر آیتم

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

کلکتور شمارش‌ها را به‌همراه آرایهٔ errors می‌دهد:

200 OK، یک آیتم رد شد
{
  "status": "ok",
  "accepted": 2,
  "rejected": 1,
  "errors": [ { "index": 1, "reason": "missing_identity" } ]
}

API مدیریت خود آرایه را زیر rejected می‌دهد:

202 Accepted، یک آیتم رد شد
{
  "accepted": 2,
  "rejected": [ { "index": 1, "reason": "missing_identity" } ]
}

یک کلمه، دو معنی. روی کلکتور rejected یک عدد است و جزئیات در errors است؛ روی API مدیریت rejected خود جزئیات است. پارسری که برای یکی نوشته شده، دیگری را «صفر خطا» می‌خواند.

index شمارهٔ خانه در آرایه‌ای است که شما فرستادید، نه در زیرمجموعهٔ پذیرفته‌شده. این عمدی است: رویدادهایی که رد می‌شوند ممکن است هنوز هیچ شناسه‌ای نداشته باشند، که خودش نصف دلیل رد شدنشان است، پس شمارهٔ خانه تنها راه پیداکردن دوبارهٔ آن‌هاست.

reason با یک کد پایدار شروع می‌شود. فهرست کامل:

کدمعنی
unknown_typetype نبود یا جزو آن پنج مقدار نیست
missing_identityنه user_id بود و نه anonymous_id
missing_event_nametype برابر track است و event خالی بود
event_name_too_longبعد از نرمال‌سازی از ۱۲۸ بایت گذشت
event_name_invalid_charsنام رویداد کاراکتر کنترلی دارد
id_too_longuser_id یا anonymous_id یا message_id از ۲۵۶ بایت گذشت
missing_previous_idtype برابر alias است و previous_id خالی بود

یکی از این‌ها با مقدار خطاساز به دنبالش می‌آید، unknown_type، چون دانستن کد بدون دانستن مقدار شما را برمی‌گرداند سراغ لاگ‌های خودتان. آیتمی با "type": "trak" این متن را تولید می‌کند: unknown_type: "trak"، نه unknown_type خالی. شش کد دیگر همیشه تنها می‌آیند و هیچ‌کدام نمی‌گویند کدام فیلد یا کدام مقدار خطا داشته.

reason را با پیشوند تطبیق بدهید، یا روی اولین ": " بشکنیدش. مقایسهٔ برابری با "unknown_type" روی unknown_type: "trak" عمل نمی‌کند، و این درست همان حالتی است که هشدارتان را برایش نوشته بودید.

وقتی همهٔ آیتم‌ها رد می‌شوند، دو در دوباره از هم جدا می‌شوند. کلکتور 200 می‌دهد و accepted در بدنه نیست، چون این فیلد وقتی صفر باشد حذف می‌شود. API مدیریت 422 all_events_rejected می‌دهد و کل آرایه را در error.details می‌گذارد، با این استدلال که مدیریت خطای یک سرویس روی کد وضعیت شاخه می‌زند و بسته‌ای که همه‌اش رد شده یک باگ سمت فراخوان‌کننده است که باید دیده شود.

هشدار با خطا فرق دارد: آیتم پذیرفته شد و چیزی در آن عوض شد. فقط کلکتور هشدار برمی‌گرداند، و حدود ۵۰ تا در هر پاسخ. POST /v1/events هشدارها را حساب می‌کند و دور می‌ریزد، پس message_id جاافتاده آنجا بی‌صدا ساخته می‌شود و هرگز به شما گفته نمی‌شود.

کد هشدارچه اتفاقی افتاد
generated_message_idmessage_id فرستاده نشد؛ ارسال دوبارهٔ این رویداد قابل حذف‌شدن نیست
timestamp_in_futureبیش از یک ساعت جلوتر از سرور؛ به زمان دریافت چسبانده شد
timestamp_too_oldقدیمی‌تر از پنجرهٔ ورود؛ به لبهٔ پنجره چسبانده شد
too_many_propertiesبیش از ۲۵۶ ویژگی؛ اضافه‌ها حذف شدند
too_many_traitsبیش از ۲۵۶ ویژگی پرونده؛ اضافه‌ها حذف شدند
unserialisable_propertyیک ویژگی قابل کدگذاری نبود و حذف شد. field نامش را می‌گوید
invalid_phoneویژگی پروندهٔ phone شمارهٔ موبایل ایرانی معتبری نبود؛ همان‌طور که فرستادید ذخیره شد و phone_operator برایش درنیامد
invalid_national_idویژگی پروندهٔ national_id از بررسی رقم کنترلی رد نشد و اصلا ذخیره نشد

#message_id، و چرا سرور باید خودش بگذاردش

message_id تنها چیزی است که ارسال دوباره را امن می‌کند.

کلکتور حذف تکراری را روی جفت حساب شما و message_id انجام می‌دهد، با SET NX در ردیس و پنجرهٔ ۴۸ ساعته. تحویل اول منتشر می‌شود. هر تکرار داخل آن پنجره با 200 {"status":"ok","accepted":1} جواب می‌گیرد، درست مثل بار اول، و هیچ چیز تازه‌ای به انبار داده نمی‌رسد. حساب هم نمی‌شود: یک کلاینت که دوباره می‌فرستد برای ما یک جست‌وجوی ردیس خرج دارد، نه یک سطر در فاکتور. تکراری داخل یک بسته هم گرفته می‌شود، پس بسته‌ای که به اشتباه یک سفارش را دو بار آورده، یک بار ذخیره می‌شود.

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

اگر message_id نفرستید، یکی برایتان ساخته می‌شود و هشدار generated_message_id می‌گیرید. آن هشدار تزئینی نیست. یعنی این رویداد هیچ محافظتی در برابر ارسال دوباره ندارد، و عددی که تغذیه می‌کند هر بار که شبکه‌تان یک بعدازظهر بد داشته باشد، بالاتر می‌رود.

شناسه را از چیزی بسازید که دیتابیس خودتان یکتا بودنش را تضمین کرده، و قطعی بسازیدش تا ارسال دوباره همان رشته را حساب کند که بار اول:

رویدادیک message_id خوب
سفارش پرداخت شدorder-8821-completed
سفارش مرجوع شدorder-8821-refunded
وضعیت مرسوله عوض شدshipment-4471-delivered
همگام‌سازی شبانهٔ پروندهprofile-44120-2026-08-07

هرگز از UUID تصادفی‌ای که در لحظهٔ ارسال ساخته می‌شود استفاده نکنید. ارسال دوباره یکی دیگر می‌سازد و حذف تکراری چیزی برای کار کردن ندارد. از زمان هم به همین دلیل استفاده نکنید.

قاعده‌های مقدار: فاصله‌های ابتدا و انتها حذف می‌شود، حداکثر ۲۵۶ بایت، و محدودیت دیگری ندارد. دامنه‌اش حساب شماست، پس با شناسهٔ مشتری دیگری تداخل نمی‌کند.

POST /v1/events هیچ حذف تکراری انجام نمی‌دهد. message_id را می‌خواند، طولش را بررسی می‌کند، روی رویداد ذخیره‌اش می‌کند، و هرگز چک نمی‌کند که پیش‌تر دیده باشدش. باز هم بفرستیدش تا بعدها بشود تکراری‌ها را پیدا و پاک کرد، ولی انتظار نداشته باشید پلتفرم جلویشان را بگیرد.

#زمان رویداد، عقب‌بردن تاریخ، و قاعدهٔ اختلاف ساعت

timestamp را نگذارید و رویداد با زمان دریافت روی سرور مهر می‌خورد. برای رویدادی که بک‌اند شما همان لحظه منتشر می‌کند این درست است و یک فیلد کمتر برای اشتباه کردن.

timestamp را وقتی بگذارید که رویداد در لحظه‌ای غیر از لحظهٔ ارسال اتفاق افتاده: یک جاب که هر ده دقیقه جدول outbox را خالی می‌کند، پرداختی که کال‌بک درگاه یک ساعت طول کشیده تا برسد، مهاجرت سفارش‌های پارسال.

سه قاعده اعمال می‌شود، به همین ترتیب.

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

قدیمی‌تر از پنجرهٔ ورود درست به لبهٔ پنجره چسبانده می‌شود، با هشدار timestamp_too_old. هرگز رد نمی‌شود. در ورود زندهٔ داده، گوشی‌ای که ساعتش خراب است نباید رویدادهایش را از دست بدهد. پیامدش برای مهاجرت داده سنگین است و ارزش صریح گفتن دارد: دو سال تاریخچه را از یک پنجرهٔ ۳۰ روزه بفرستید و همهٔ رویدادها روی یک لحظهٔ واحد می‌نشینند، پذیرفته‌شده، با پاسخ موفق HTTP، و اولین نشانه‌اش ماه‌ها بعد یک قیف است که هیچ معنایی ندارد.

پنجره یک ثابت سراسری نیست. روی کلکتور همان نگهداشت رویداد خود حساب است: حسابی که رویدادها را برای همیشه نگه می‌دارد ۳۶۵۰ روز می‌گیرد، حسابی که ۹۰ روز تنظیم کرده ۹۰ روز می‌گیرد، و حسابی که روی کف ۳۰ روزه نشسته باز هم همان پیش‌فرض کامل ۳۰ روزه را دارد. روی POST /v1/events این پنجره هرگز پر نمی‌شود، پس همیشه همان پیش‌فرض ثابت ۳۰ روزه اعمال می‌شود، هر چیزی که تنظیم نگهداشت شما بگوید.

اگر دارید تاریخچه مهاجرت می‌دهید، از POST /v1/batch کلکتور استفاده کنید و اول تنظیم نگهداشت رویداد حسابتان را ببینید. POST /v1/events هرچه قدیمی‌تر از ۳۰ روز باشد را می‌چسباند بدون اینکه چیزی را رد کند.

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

sent_at برای گوشی‌ای وجود دارد که ساعتش غلط است. SDK می‌گوید فکر می‌کند بسته را چه زمانی فرستاده؛ سرور آن را با زمان واقعی رسیدن مقایسه می‌کند؛ اختلاف را روی زمان رویداد اعمال می‌کند. دستگاهی که ساعتش دو ساعت عقب است و می‌گوید رویداد ساعت ۰۸:۰۰ رخ داده و ساعت ۱۰:۰۰ فرستاده، و بسته‌اش ساعت ۱۲:۰۰ می‌رسد، رویدادش با ۱۰:۰۰ ذخیره می‌شود. تصحیح فقط وقتی اجرا می‌شود که اختلاف از یک دقیقه بیشتر باشد، و فقط اگر زمان تصحیح‌شده هنوز داخل پنجره بیفتد. هیچ هشداری صادر نمی‌شود.

ساعت سرور شما درست است. پس یا sent_at را نگذارید، یا برابر همان لحظه‌ای بگذاریدش که در عمل می‌فرستید. کاری که نباید بکنید کپی‌کردن timestamp داخل sent_at است، که نوشتنش طبیعی به‌نظر می‌رسد و فاجعه است: برای رویدادی مربوط به سه روز پیش، فاصلهٔ «فرستاده» تا «رسیده» سه روز حساب می‌شود، همان سه روز به زمان رویداد اضافه می‌شود، و رویداد روی الان می‌نشیند. کل مهاجرت شما روی امروز فرو می‌ریزد.

عقب‌بردن درست تاریخ: بدون sent_at
{
  "batch": [
    {
      "type": "track",
      "message_id": "order-7702-completed",
      "event": "order_completed",
      "user_id": "u_39900",
      "timestamp": "2026-05-14T11:02:00Z",
      "properties": { "order_id": "7702", "revenue": 1250000 }
    }
  ]
}

#کدام کد وضعیت را می‌شود دوباره فرستاد

کددوباره بفرستم؟چرا
200 / 202نهکار کرد. قبل از رفتن، خطای هر آیتم را بخوانید
400نهبدنه خراب است و دفعهٔ بعد هم خراب خواهد بود
401نهکلید اشتباه است. تکرار یک سطر لاگ می‌سازد، نه یک راه‌حل
402نهحساب از سقف گذشته. تا کسی پرداخت نکند چیزی عوض نمی‌شود
403نهکلید profile.write ندارد. یک نفر باید کلید دیگری صادر کند
404نهروی API مدیریت یعنی مسیر ورود داده اینجا سرو نمی‌شود
413نهبسته بزرگ است. تکه‌اش کنید؛ همان بدنه هرگز رد نمی‌شود
422نههمهٔ رویدادها رد شدند. error.details را بخوانید
429بله، بعد از Retry-After: 60فقط روی API مدیریت، بودجهٔ این کلید تمام شده
503بلهمال ماست نه شما. پایین‌تر را ببینید
خطای شبکه بدون پاسخبلهممکن است درخواست هرگز نرسیده باشد

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

روی هیچ‌کدام از دو در، پاسخ 402 عدد ندارد. مقدار مصرف‌شده و سقف سمت ما حساب می‌شوند و همان‌جا دور ریخته می‌شوند، پس «چقدر رد کردم» را فقط از صفحهٔ صورتحساب می‌شود پرسید، نه از این پاسخ.

روی 402 کل بسته رد می‌شود، هرگز بخشی از آن. پذیرش جزئی شما را در وضعیتی می‌گذاشت که نمی‌دانستید کدام آیتم‌ها را باید دوباره بفرستید.

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

#پاسخ ۵۰۳، و اینکه دور انداختنش یعنی از دست دادن داده

503 تنها کدی است که می‌گوید خرابی سمت ماست.

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

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

کنارش هدر X-Segmentic-Trace را هم لاگ کنید: شانزده رقم هگز که هر دو در روی هر پاسخی می‌گذارند و در هیچ بدنهٔ خطایی تکرار نمی‌شود. برای پیگیری یک درخواست مشخص، تنها چیزی است که به کار ما می‌آید. اگر شما خودتان این هدر را روی درخواست بگذارید و مقدارش هگز با طول معقول باشد، همان برگردانده می‌شود.

روی کلکتور دو نوع 503 هست و فقط از روی پاسخ می‌شود از هم تشخیصشان داد:

Retry-Afterپیامیعنی چه
جست‌وجوی کلید شکست خورد5cannot verify the write key right now; retryدرخواست هرگز به خط لوله نرسید. فرستادن دوبارهٔ همان بدنه درست‌ترین کار است
انتشار شکست خوردنداردtemporarily unavailable, please retryهم باس رویداد و هم بافر دیسک محلی شکست خوردند

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

در 503 ناشی از شکست انتشار، مقدار message_id پیش‌تر در پنجرهٔ حذف تکراری ثبت شده است. ارسال دوباره با همان message_id تا ۴۸ ساعت بعد با 200 {"status":"ok","accepted":1} جواب می‌گیرد و ذخیره نمی‌شود: کلکتور نمی‌تواند این ارسال دوباره را از یک تکراری واقعی تشخیص بدهد. برای اینکه آن رویداد وارد شود، با یک message_id متفاوت بفرستیدش و بپذیرید که همین یک رویداد محافظتی در برابر تکرار ندارد. این تنها حالتی است که استفادهٔ دوباره از همان شناسه غلط است.

روی API مدیریت، 503 ingest_unavailable یعنی صف در دسترس نبود. بسته را دوباره بفرستید. چون آن آدرس حذف تکراری ندارد، 503 بعد از یک انتشار نیمه‌تمام هرچه رفته را دو بار می‌شمارد، که یک دلیل دیگر است برای ترجیح دادن کلکتور در هر چیزی که پول در آن هست.

503 budget_unavailable خرابی دیگری با همان کد است: سرویس بودجه خطا داد و API به‌جای اینکه یک کلاینت بی‌شمارش را در حلقه رها کند، بسته عمل می‌کند. با عقب‌نشینی دوباره بفرستید. این یکی همهٔ مسیرهای api.segmentic.net را می‌گیرد، از جمله GET /v1/whoami را.

#ترتیب

یک تضمین، و ارزش دارد بدانید تا کجا می‌رود.

هر رویداد با کلید پارتیشن user_id خودش روی باس گذاشته می‌شود، و اگر user_id نداشته باشد با anonymous_id. پس همهٔ رویدادهای یک نفر روی یک پارتیشن می‌نشینند، و مصرف‌کننده‌هایی که برای هر نفر حالت نگه می‌دارند (به‌روزرسانی پرونده، حالت سناریو، دوختن نشست) آن‌ها را به همان ترتیبی می‌بینند که باس پذیرفته، بدون هیچ هماهنگی بین پارتیشن‌ها.

چه چیزی از این نتیجه می‌شود و چه چیزی نمی‌شود:

  • داخل یک بسته، آیتم‌ها به ترتیب آرایه منتشر می‌شوند، پس دو رویداد یک نفر در یک بسته ترتیبشان را حفظ می‌کنند.
  • بین دو درخواست، ترتیب همان ترتیب پذیرفته‌شدن درخواست‌هاست. دو درخواست همزمان از دو ورکر هیچ ترتیبی نسبت به هم ندارند.
  • عوض‌شدن هویت یعنی عوض‌شدن پارتیشن. رویدادی که با anonymous_id رفته و رویداد بعدی که با user_id رفته روی دو پارتیشن‌اند و هیچ ترتیبی نسبت به هم ندارند. alias و identify برای همین وجود دارند؛ هویت را ببینید.
  • ترتیب در قطعی باس حفظ نمی‌شود. رویدادهایی که در لاگ محلی بافر شده‌اند روی یک تایمر و حدود هر پنج ثانیه پخش می‌شوند، پس رویدادی که در زمان قطعی پذیرفته شده می‌تواند بعد از رویدادهای بعد از خودش به باس برسد.

اگر ترتیب دو رویداد برای یک گزارش مهم است، به ترتیب ارسال تکیه نکنید. روی هر دو timestamp بگذارید.

#سقف ماهانه و بودجهٔ درخواست

دو محدودیت متفاوت، روی دو در متفاوت، با دو کد وضعیت متفاوت.

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

بودجهٔ درخواست فقط روی api.segmentic.net است. وزنی است نه شمارشی، چون یک فراخوان یک ساختار را می‌خواند و فراخوان بعدی کل انبار داده را اسکن می‌کند. POST /v1/events پنج واحد خرج دارد. سهمیه ۶۰۰ واحد در دقیقه است و به‌ازای هر کلید API حساب می‌شود نه هر حساب، تا یک ایجنت از کنترل خارج‌شده نتواند بودجه‌ای را که خط لولهٔ سفارش شما به آن وابسته است تمام کند. یعنی ۱۲۰ فراخوان ورود داده در دقیقه برای هر کلید، یا ۶۰۰۰۰ رویداد در دقیقه با بیشترین اندازهٔ بسته.

پنجره یک دقیقهٔ تقویمی ثابت است نه لغزان، و روی بودجه هیچ هدر X-RateLimit-* وجود ندارد. نمی‌توانید بپرسید چقدر از آن برایتان مانده، و GET /v1/whoami هم گزارشش نمی‌کند. وقتی تمام شد 429 می‌گیرید با Retry-After: 60 که محافظه‌کارانه است: ممکن است دقیقه زودتر بچرخد.

کلکتور نه بودجهٔ درخواست دارد و نه هیچ نوع محدودکنندهٔ نرخی. تنها کنترل حجم روی آن، همان سقف ماهانه است.

#User-Agent شما، و پرچم ربات

کلکتور آی‌پی و User-Agent را از خود اتصال برمی‌دارد، هرگز از بدنه، تا کلاینت نتواند موقعیت جغرافیایی یا دستگاه خودش را جعل کند. از مرورگر، این تنها راه فهمیدن این است که بازدیدکننده روی چه چیزی است. از بک‌اند شما یعنی رویداد با مهر دیتاسنتر شما و کلاینت HTTP شما ذخیره می‌شود.

بیشترش بی‌ضرر است. مقدار browser_name می‌شود python-requests یا Go-http-client که نامرتب است و چیزی به شما نمی‌گوید که ندانید.

یک حالت بی‌ضرر نیست. تجزیه‌کنندهٔ User-Agent وقتی رشتهٔ User-Agent یک نشانی وب داخلش داشته باشد، درخواست را ربات علامت می‌زند، و جدا از آن وقتی نام تجزیه‌شده رشتهٔ bot را در خود داشته باشد. همهٔ گزارش‌ها، همهٔ کاشی‌های داشبورد و کامپایلر سگمنت روی is_bot = 0 فیلتر می‌کنند. پس یک سرویس خوش‌رفتار که مؤدبانه خودش را همان‌طور معرفی می‌کند که از یک کلاینت HTTP انتظار می‌رود:

User-Agent: myshop-orders/1.0 (+https://myshop.ir)

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

یک عبارت ساده بدون نشانی وب بفرستید، و رشتهٔ bot را در نام نیاورید:

User-Agent: myshop-orders/1.0

نفرستادن این هدر هم جواب می‌دهد: با نبودن User-Agent تجزیه‌کننده اجرا نمی‌شود و هیچ‌کدام از فیلدهای دستگاه دست نمی‌خورند. POST /v1/events این هدر را هرگز نمی‌خواند، پس تمام این بخش به آن ربطی ندارد.

#چیزهایی که وجود ندارند

نوشته شده چون فهمیدنشان با آزمایش، یک بعدازظهر خرج دارد.

  • فشرده‌سازی نیست. هیچ‌کدام از دو آدرس Content-Encoding را نمی‌خوانند، پس بدنهٔ gzip شده 400 malformed JSON می‌گیرد.
  • روی POST /v1/events نه حذف تکراری هست و نه کلید idempotency. هدر Idempotency-Key فقط روی یک آدرس در کل این API کار می‌کند، POST /v1/messages، و جای دیگری نه.
  • POST /v1/events هشدار برنمی‌گرداند. هشدارها حساب و دور ریخته می‌شوند.
  • هیچ راهی برای خواندن دوبارهٔ یک رویداد، ویرایش یا حذف آن نیست. GET /v1/events وجود ندارد. اصلاح با فرستادن یک رویداد جبرانی انجام می‌شود.
  • روی 503 ناشی از شکست انتشار در کلکتور، هدر Retry-After نیست. فقط 503 جست‌وجوی کلید آن را دارد.
  • روی کلکتور هیچ محدودیت نرخی نیست: نه سقف در ثانیه، نه کنترل انفجار ترافیک، نه سقف همزمانی برای هر حساب.
  • هیچ آدرس مهاجرت داده‌ای از api.segmentic.net در دسترس نیست. آدرس POST /v1/import/events که زمان خارج از پنجره را به‌جای چسباندن رد می‌کند، فقط روی API داخلی داشبورد ثبت شده و عمومی مسیریابی نمی‌شود. /v1/batch کلکتور و پنجرهٔ نگهداشت خود حساب، تنها راه دادهٔ قدیمی‌اند.
  • در نسخهٔ مستقرشده مکان‌یابی از روی آی‌پی نیست. مقادیر country و region و city فقط از چیزی پر می‌شوند که شما در context.location می‌فرستید.
  • روی api.segmentic.net پاسخ preflight برای CORS نیست. مرورگر نمی‌تواند API مدیریت را صدا بزند؛ کلید نوشتن و کلکتور برای همین‌اند.
  • روی بودجهٔ درخواست هدرهای X-RateLimit-Limit و X-RateLimit-Remaining نیست.
  • پاسخ 402 کلکتور انگلیسی ندارد. همیشه فارسی است.
  • برای ترافیک بسته‌ای، دیباگر زندهٔ رویداد کار نمی‌کند. دیباگر داشبورد ارسال‌های تک‌رویدادی و رویدادهای وبهوک را ضبط می‌کند؛ /v1/batch چیزی به آن نمی‌دهد، پس یک سرویس سروری که بسته می‌فرستد آنجا چیزی نمی‌بیند.

#یک برنامهٔ کامل به زبان Go

یک بسته را به کلکتور می‌فرستد، شکست‌هایی را که مال ماست دوباره می‌فرستد، و خطای هر آیتم را می‌خواند. فقط کتابخانهٔ استاندارد.

main.go
// Sends completed orders to Segmentic from a Go backend.
//
//	export SEGMENTIC_WRITE_KEY=wk_seg_...
//	go run main.go
package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
)

const (
	ingestURL    = "https://in.segmentic.net/v1/batch"
	maxBatchSize = 500
)

type envelope struct {
	Type       string         `json:"type"`
	MessageID  string         `json:"message_id"`
	Event      string         `json:"event,omitempty"`
	UserID     string         `json:"user_id"`
	Timestamp  *time.Time     `json:"timestamp,omitempty"`
	Properties map[string]any `json:"properties,omitempty"`
	Traits     map[string]any `json:"traits,omitempty"`
}

// No sent_at field. It is for correcting a wrong device clock, and setting it
// from a server whose clock is right can only move timestamps that were
// already correct.
type batch struct {
	Batch   []envelope     `json:"batch"`
	Context map[string]any `json:"context,omitempty"`
}

type itemError struct {
	Index  int    `json:"index"`
	Reason string `json:"reason"`
}

type reply struct {
	Status   string      `json:"status"`
	Accepted int         `json:"accepted"`
	Rejected int         `json:"rejected"`
	Errors   []itemError `json:"errors"`
	Message  string      `json:"message"`
}

// errRetryable marks a failure that a second attempt can fix.
var errRetryable = errors.New("segmentic: temporarily unavailable")

// errBurnt marks the one 503 where the message_id has already been consumed:
// the publish failed after de-duplication recorded the ids, so an identical
// retry is answered 200 and stored nowhere.
var errBurnt = errors.New("segmentic: publish failed; message ids are spent")

func post(client *http.Client, key string, events []envelope) (reply, error) {
	if len(events) > maxBatchSize {
		return reply{}, fmt.Errorf("segmentic: %d events, limit %d", len(events), maxBatchSize)
	}

	body, err := json.Marshal(batch{
		Batch:   events,
		Context: map[string]any{"locale": "fa-IR"},
	})
	if err != nil {
		return reply{}, err
	}

	req, err := http.NewRequest(http.MethodPost, ingestURL, bytes.NewReader(body))
	if err != nil {
		return reply{}, err
	}
	req.Header.Set("Authorization", "Bearer "+key)
	req.Header.Set("Content-Type", "application/json")
	// A plain token. A User-Agent containing a URL makes the parser flag the
	// event as a bot, and every report filters bots out.
	req.Header.Set("User-Agent", "myshop-orders/1.0")

	res, err := client.Do(req)
	if err != nil {
		// The request may never have arrived, so the ids are still free.
		return reply{}, fmt.Errorf("%w: %v", errRetryable, err)
	}
	defer res.Body.Close()

	raw, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
	if err != nil {
		return reply{}, fmt.Errorf("%w: %v", errRetryable, err)
	}

	var out reply
	if err := json.Unmarshal(raw, &out); err != nil {
		return reply{}, fmt.Errorf("segmentic: unreadable reply, status %d: %s", res.StatusCode, raw)
	}

	switch res.StatusCode {
	case http.StatusOK:
		return out, nil
	case http.StatusServiceUnavailable:
		if res.Header.Get("Retry-After") != "" {
			// The key lookup failed. Nothing reached the pipeline.
			return out, fmt.Errorf("%w: %s", errRetryable, out.Message)
		}
		return out, fmt.Errorf("%w: %s", errBurnt, out.Message)
	default:
		// 400, 401, 402 and 413 all say the same thing on a second attempt.
		return out, fmt.Errorf("segmentic: %d %s", res.StatusCode, out.Message)
	}
}

func main() {
	key := os.Getenv("SEGMENTIC_WRITE_KEY")
	if key == "" {
		log.Fatal("SEGMENTIC_WRITE_KEY is not set")
	}

	paidAt := time.Now().UTC().Add(-45 * time.Minute)
	events := []envelope{
		{
			Type: "track",
			// Derived from the order, so a retry computes the same string.
			MessageID: "order-8821-completed",
			Event:     "order_completed",
			UserID:    "u_44120",
			Timestamp: &paidAt,
			Properties: map[string]any{
				"order_id": "8821",
				"revenue":  4800000,
				"currency": "IRR",
				"city":     "تهران",
			},
		},
		{
			Type:      "identify",
			MessageID: "profile-44120-v7",
			UserID:    "u_44120",
			Traits: map[string]any{
				"phone":      "09123456789",
				"first_name": "سارا",
				"city":       "تهران",
			},
		},
	}

	client := &http.Client{Timeout: 15 * time.Second}

	var out reply
	var err error
	for attempt := 1; attempt <= 5; attempt++ {
		out, err = post(client, key, events)
		if err == nil || !errors.Is(err, errRetryable) {
			break
		}
		wait := time.Duration(1<<attempt) * time.Second
		log.Printf("attempt %d failed (%v); waiting %s", attempt, err, wait)
		time.Sleep(wait)
	}
	if err != nil {
		log.Fatalf("segmentic: giving up: %v", err)
	}

	log.Printf("accepted %d, rejected %d", out.Accepted, out.Rejected)
	for _, e := range out.Errors {
		// Split on ": " because unknown_type carries the offending value.
		code, _, _ := strings.Cut(e.Reason, ": ")
		log.Printf("item %d (%s) rejected: %s", e.Index, events[e.Index].MessageID, code)
	}
}

#یک برنامهٔ کامل به زبان Python

جدول outbox را خالی می‌کند و در تکه‌های ۵۰۰ تایی به کلکتور می‌فرستد، با زمان‌های عقب‌برده و بدون sent_at. به requests نیاز دارد.

send_orders.py
#!/usr/bin/env python3
"""Send an outbox of paid orders to Segmentic.

    pip install requests
    export SEGMENTIC_WRITE_KEY=wk_seg_...
    python send_orders.py
"""

import os
import sys
import time
from datetime import datetime, timedelta, timezone

import requests

INGEST_URL = "https://in.segmentic.net/v1/batch"
MAX_BATCH = 500
# Everything else means the payload or the credential is wrong, and a second
# attempt sends the same wrong thing.
RETRYABLE = {408, 500, 502, 503, 504}


def rfc3339(moment: datetime) -> str:
    """The only timestamp format the ingest endpoint decodes."""
    return moment.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def chunks(items, size):
    for start in range(0, len(items), size):
        yield items[start:start + size]


def post_batch(session: requests.Session, key: str, events: list) -> dict:
    """POST one batch. Returns the decoded reply or raises."""
    if len(events) > MAX_BATCH:
        raise ValueError(f"{len(events)} events, limit {MAX_BATCH}")

    response = session.post(
        INGEST_URL,
        # No sent_at anywhere: these events are backdated, and sent_at would
        # be read as clock skew and move every one of them to now.
        json={"batch": events, "context": {"locale": "fa-IR"}},
        headers={
            "Authorization": f"Bearer {key}",
            "Content-Type": "application/json",
            # No URL in the token: a User-Agent containing one makes the
            # event count as bot traffic, which every report filters out.
            "User-Agent": "myshop-outbox/1.0",
        },
        timeout=20,
    )

    try:
        body = response.json()
    except ValueError:
        body = {"message": response.text[:500]}

    if response.status_code == 200:
        return body
    if response.status_code in RETRYABLE:
        raise ConnectionError(f"{response.status_code}: {body.get('message')}")
    raise RuntimeError(f"{response.status_code}: {body.get('message')}")


def send_with_retries(session, key, events, attempts=5):
    for attempt in range(1, attempts + 1):
        try:
            return post_batch(session, key, events)
        except (ConnectionError, requests.RequestException) as exc:
            if attempt == attempts:
                raise
            wait = 2 ** attempt
            print(f"attempt {attempt} failed ({exc}); waiting {wait}s", file=sys.stderr)
            time.sleep(wait)


def main() -> int:
    key = os.environ.get("SEGMENTIC_WRITE_KEY")
    if not key:
        print("SEGMENTIC_WRITE_KEY is not set", file=sys.stderr)
        return 1

    # Stand-in for the rows your own outbox query returns.
    now = datetime.now(timezone.utc)
    orders = [
        {"id": 8821, "user": "u_44120", "rial": 4800000, "paid": now - timedelta(hours=3)},
        {"id": 8822, "user": "u_39900", "rial": 1250000, "paid": now - timedelta(hours=2)},
        {"id": 8823, "user": "", "rial": 990000, "paid": now - timedelta(hours=1)},
    ]

    events = [
        {
            "type": "track",
            # Deterministic, so a retry produces the same id and the second
            # delivery is de-duplicated instead of counted again.
            "message_id": f"order-{order['id']}-completed",
            "event": "order_completed",
            "user_id": order["user"],
            "timestamp": rfc3339(order["paid"]),
            "properties": {
                "order_id": str(order["id"]),
                "revenue": order["rial"],
                "currency": "IRR",
            },
        }
        for order in orders
    ]

    session = requests.Session()
    failures = 0

    for part in chunks(events, MAX_BATCH):
        reply = send_with_retries(session, key, part)
        print(f"accepted {reply.get('accepted', 0)}, rejected {reply.get('rejected', 0)}")

        for problem in reply.get("errors", []):
            # Prefix match: unknown_type arrives as 'unknown_type: "trak"'.
            code = problem["reason"].split(": ", 1)[0]
            bad = part[problem["index"]]
            print(f"  {bad['message_id']}: {code}", file=sys.stderr)
            failures += 1

        for note in reply.get("warnings", []):
            print(f"  warning {note['code']}: {note.get('note', '')}", file=sys.stderr)

    return 1 if failures else 0


if __name__ == "__main__":
    sys.exit(main())

#یک برنامهٔ کامل به زبان PHP

مسیر API مدیریت، برای بک‌اندی که همین حالا یک کلید sk_seg_ دارد. فقط به ext-curl و ext-json نیاز دارد.

send_events.php
<?php
/**
 * Send events to Segmentic's management API from PHP.
 *
 *   SEGMENTIC_API_KEY=sk_seg_... php send_events.php
 *
 * This endpoint does not de-duplicate. If this script can run twice over the
 * same rows, mark them as sent in your own database inside a transaction.
 */

declare(strict_types=1);

const EVENTS_URL = 'https://api.segmentic.net/v1/events';
const MAX_BATCH  = 500;

/**
 * POST one batch. Returns the decoded 202 body.
 *
 * @throws RuntimeException with the HTTP status as its code.
 */
function segmenticSend(string $key, array $events): array
{
    if (count($events) > MAX_BATCH) {
        throw new RuntimeException(count($events) . ' events, limit ' . MAX_BATCH, 413);
    }

    $payload = json_encode(
        ['events' => $events],
        JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
    );

    $curl = curl_init(EVENTS_URL);
    curl_setopt_array($curl, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_POSTFIELDS     => $payload,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $key,
            'Content-Type: application/json',
        ],
    ]);

    $raw    = curl_exec($curl);
    $status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    $error  = curl_error($curl);
    curl_close($curl);

    if ($raw === false) {
        // No response at all. Treated as retryable: the request may never
        // have arrived.
        throw new RuntimeException('transport: ' . $error, 503);
    }

    $body = json_decode($raw, true);
    if (!is_array($body)) {
        throw new RuntimeException('unreadable reply: ' . substr($raw, 0, 300), $status);
    }

    if ($status === 202) {
        return $body;
    }

    // error.code is the contract. error.message is prose and will be reworded.
    // On ten other routes of this API, error is a plain string instead, so
    // read its type before reaching into it.
    $code = is_array($body['error'] ?? null)
        ? ($body['error']['code'] ?? 'unknown')
        : (string) ($body['error'] ?? 'unknown');

    throw new RuntimeException($code, $status);
}

$key = getenv('SEGMENTIC_API_KEY');
if ($key === false || $key === '') {
    fwrite(STDERR, "SEGMENTIC_API_KEY is not set\n");
    exit(1);
}

// No batch-level context or sent_at on this endpoint: those fields exist only
// on the collector, so anything shared has to be repeated per item.
$events = [
    [
        'type'       => 'track',
        'message_id' => 'order-8821-completed',
        'event'      => 'order_completed',
        'user_id'    => 'u_44120',
        'timestamp'  => gmdate('Y-m-d\TH:i:s\Z', time() - 1800),
        'properties' => [
            'order_id' => '8821',
            'revenue'  => 4800000,
            'currency' => 'IRR',
            'city'     => 'تهران',
        ],
        'context'    => ['locale' => 'fa-IR'],
    ],
    [
        'type'       => 'identify',
        'message_id' => 'profile-44120-v7',
        'user_id'    => 'u_44120',
        'traits'     => ['phone' => '09123456789', 'city' => 'تهران'],
        'context'    => ['locale' => 'fa-IR'],
    ],
];

$attempt = 0;
while (true) {
    $attempt++;
    try {
        $reply = segmenticSend($key, $events);
        break;
    } catch (RuntimeException $e) {
        // 429 carries Retry-After: 60. 503 is ours. Everything else is fixed
        // by changing the request, not by repeating it.
        $retryable = in_array($e->getCode(), [429, 503], true);
        if (!$retryable || $attempt >= 5) {
            fwrite(STDERR, 'segmentic refused: ' . $e->getCode() . ' ' . $e->getMessage() . "\n");
            exit(1);
        }
        $wait = $e->getCode() === 429 ? 60 : 2 ** $attempt;
        fwrite(STDERR, "attempt {$attempt}: {$e->getMessage()}; waiting {$wait}s\n");
        sleep($wait);
    }
}

printf("accepted %d\n", $reply['accepted'] ?? 0);

foreach ($reply['rejected'] ?? [] as $item) {
    // Prefix match: unknown_type arrives as 'unknown_type: "trak"'.
    $code = explode(': ', $item['reason'], 2)[0];
    $bad  = $events[$item['index']]['message_id'] ?? '(no message_id)';
    fwrite(STDERR, "rejected {$bad}: {$code}\n");
}

#توسعهٔ محلی

کلکتور روی http://localhost:8080 گوش می‌دهد و /v1/batch را همان‌جا سرو می‌کند، بدون هیچ تغییری در بدنه.

API مدیریت داستان دیگری دارد. روی آدرسی سرو می‌شود که در PUBLIC_API_ADDR نوشته شده، و مقدار پیش‌فرض آن خالی است، پس روی یک نصب تازه API عمومی سرو نمی‌شود. هیچ چیزی گوش نمی‌دهد، و اولین نشانه‌اش یک connection refused است که شبیه مشکل شبکه به نظر می‌رسد. مقدارش را بگذارید، ری‌استارت کنید، و قبل از اینکه دنبال هر چیز دیگری بگردید با GET /v1/status چک کنید.

آیا API عمومی بالاست؟
curl -sS http://localhost:8082/v1/status
JSON
{ "status": "ok", "service": "api", "version": "dev" }

GET /v1/status تنها مسیری روی API مدیریت است که کلید نمی‌خواهد و از قطعی ردیس جان سالم به در می‌برد. بقیه، از جمله GET /v1/whoami، از بودجهٔ درخواست رد می‌شوند و بودجه بسته عمل می‌کند.

#بعدش کجا

  • رویدادها برای کل ساختار پاکت، از جمله تمام شیء context.
  • فرهنگ رویدادها برای نام‌ها و ویژگی‌های استانداردی که قیف‌های آماده را بدون پیکربندی به کار می‌اندازند.
  • هویت برای user_id و anonymous_id و alias.
  • خطاها برای همهٔ کدها روی هر دو سطح.
  • محدودیت‌ها برای هر عددی که پلتفرم شما را به آن پایبند می‌کند.
  • API مدیریت برای سگمنت و کمپین و گزارش.
قبلیثبت دستگاهبعدیکاتالوگ محصولات

در این صفحه

  • دو در، و یکی نیستند
  • بک‌اند شما کدام را باید بردارد
  • کلکتور: POST /v1/batch
  • API مدیریت: POST /v1/events
  • بسته‌بندی و سقف آن
  • شکست بخشی، و خواندن خطای هر آیتم
  • message_id، و چرا سرور باید خودش بگذاردش
  • زمان رویداد، عقب‌بردن تاریخ، و قاعدهٔ اختلاف ساعت
  • کدام کد وضعیت را می‌شود دوباره فرستاد
  • پاسخ ۵۰۳، و اینکه دور انداختنش یعنی از دست دادن داده
  • ترتیب
  • سقف ماهانه و بودجهٔ درخواست
  • User-Agent شما، و پرچم ربات
  • چیزهایی که وجود ندارند
  • یک برنامهٔ کامل به زبان Go
  • یک برنامهٔ کامل به زبان Python
  • یک برنامهٔ کامل به زبان PHP
  • توسعهٔ محلی
  • بعدش کجا

سگمنتیک

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