تعریف رویداد: چه چیزی بفرستیم و با چه نامی
قاعده نامگذاری، انتخاب ویژگیها و ساخت یک tracking plan پایدار برای گزارش و سگمنت.
رویداد چیزی است که یک نفر انجام داد. اسم دارد، لحظه دارد، و چند ویژگی دارد که همان لحظه درست بودند. تمام تصمیمهای بعدی شما، هر سگمنت، هر کمپین، هر قیف، روی همین سه چیز سوار میشود؛ و بعضی از این تصمیمها را نمیشود پس گرفت. این صفحه دقیقا میگوید کد چه کاری با آنچه میفرستید میکند. اگر دنبال دستور کار عملی هستید، یعنی اینکه فراخوانی را کجای کد خودتان بگذارید و چطور بررسی کنید که رسیده، گذاشتن رویداد همان صفحه است.
رویداد چیست و چه چیزی نیست
رویداد وضعیت نیست. «موجودی کیف پول این کاربر ۱۲۰۰۰ تومان است» رویداد نیست، ویژگی پرونده است و با identify فرستاده میشود. «کاربر کیف پولش را ۱۲۰۰۰ تومان شارژ کرد» رویداد است. فرق این دو در بخش ویژگی رویداد یا ویژگی پرونده کامل باز شده، چون بیشترین اشتباه همانجا اتفاق میافتد.
رویداد بعد از نوشتهشدن عوض نمیشود. هیچ endpointی برای ویرایش یا حذف یک رویداد وجود ندارد. سه چیز سطر را از جدول برمیدارد: سیاست نگهداری تنانت شما، TTL چهارصدروزه خود جدول رویدادها، و پاکسازی یک شخص شناساییشده. یعنی هر تصمیمی که این صفحه از شما میخواهد، روی دادهای اثر دارد که از این لحظه به بعد میآید، نه روی چیزی که دیروز آمده است.
ترافیک ربات دور ریخته نمیشود. ذخیره میشود و ستون is_bot روی آن یک میشود، و گزارشها بهصورت پیشفرض کنارش میگذارند. اگر عددی را دور ریخته بودیم، بعدا هیچکس نمیتوانست ثابت کند که آن روز واقعا چه اتفاقی افتاده است.
یک رویداد باید دستکم یکی از user_id یا anonymous_id را داشته باشد، وگرنه با کد missing_identity رد میشود. هر دو حداکثر ۲۵۶ بایتاند.
پنج نوع پیام
پنج نوع پیام وجود دارد و هرکدام مسیر خودش را دارد. نام رویدادی که ذخیره میشود، همیشه از خود شما نمیآید:
| مسیر | نوع | نامی که ذخیره میشود |
|---|---|---|
POST /v1/track | track | همان event که فرستادهاید. اجباری است |
POST /v1/page | page | همان event، و اگر خالی باشد page_viewed |
POST /v1/screen | screen | همان event، و اگر خالی باشد screen_viewed |
POST /v1/identify | identify | همیشه identify. هرچه در event بفرستید دور ریخته میشود |
POST /v1/alias | alias | همیشه alias. مثل بالا |
POST /v1/batch | هر پنجتا | هر آیتم type خودش را دارد |
روی پنج مسیر تکرویدادی، مسیر تعیینکننده است و فیلد type داخل بدنه نادیده گرفته میشود. اگر {"type":"identify"} را به /v1/track بفرستید، یک رویداد track ذخیره میشود. این عمدی است: در غیر این صورت یک بار اشتباه تایپ کردن در کد شما باعث میشد بار identify بیسروصدا بهعنوان رویداد پذیرفته شود.
داخل /v1/batch قضیه برعکس است، چون مسیر یکی است و آیتمها فرق دارند: type هر آیتم خوانده میشود و اگر ناشناخته باشد فقط همان آیتم با کد unknown_type رد میشود، نه کل دسته.
ویژگیهای identify (که به آنها «ویژگی» یا trait میگوییم) روی جدول رویدادها ذخیره نمیشوند. رویداد identify با ویژگیهای خالی در جدول مینشیند و مقدار ویژگیها فقط به پرونده کاربر میرود. اگر میخواهید در گزارش رویدادی ببینید که چه چیزی عوض شد، آن را بهعنوان یک track جدا هم بفرستید.
curl -X POST https://in.segmentic.net/v1/track \
-H "Content-Type: application/json" \
-H "Authorization: Bearer wk_seg_..." \
-d '{
"message_id": "9f1c2b70-3a4d-4f2e-9a1b-0c7d8e5f6a21",
"user_id": "u_88123",
"event": "order_completed",
"timestamp": "2026-08-07T09:14:22.310Z",
"properties": {
"order_id": "A-100294",
"revenue": 2450000,
"currency": "IRR",
"item_count": 3,
"payment_method": "gateway"
},
"context": {
"locale": "fa-IR",
"timezone": "Asia/Tehran",
"library": { "name": "segmentic-js", "version": "1.0.0" }
}
}'
{"status":"ok","accepted":1}
کلید نوشتن را میتوانید با هدر Authorization: Bearer wk_seg_...، یا هدر X-Segmentic-Key، یا پارامتر ?write_key= بدهید. سومی فقط برای sendBeacon و بیکن تصویری وجود دارد که نمیتوانند هدر بگذارند.
اگر message_id نفرستید، سرور یکی میسازد و یک هشدار برمیگرداند:
{"status":"ok","accepted":1,"warnings":[{"code":"generated_message_id","field":"message_id","note":"no message_id sent; retries of this event cannot be de-duplicated"}]}
آن هشدار جدی است. message_id تنها چیزی است که تکرار را بیخطر میکند: SDK روی شبکه موبایل ایران دوباره میفرستد، و بدون شناسه ثابت، شمارش خرید مشتری بیصدا دو برابر میشود. حذف تکراری per-tenant است و دو تنانت میتوانند از یک message_id استفاده کنند.
قاعده نامگذاری
این کل قاعده است، و کوتاهتر از چیزی است که انتظار دارید. نام رویداد TrimSpace میشود، از نرمالسازی فارسی میگذرد (بخش متن فارسی)، و بعد فقط دو چیز بررسی میشود:
| قاعده | حد | کد رد |
|---|---|---|
| طول بعد از نرمالسازی | ۱۲۸ بایت | event_name_too_long |
| کاراکتر کنترلی یونیکد | هیچکدام مجاز نیست | event_name_invalid_chars |
| وجود نام روی track | اجباری | missing_event_name |
همین. بدون کوچککردن حروف، بدون snake_case کردن، بدون هیچ فیلتری روی کاراکترها. یعنی فاصله، اسلش، علامت سؤال، حروف فارسی، ایموجی، حروف بزرگ و کوچک قاطی، و یک آدرس کامل، همه نامهای قانونی رویداد هستند.
دو نتیجه که آدمها با آن غافلگیر میشوند:
Order Completedوorder_completedوorder completedسه رویداد جدا هستند، برای همیشه. هیچچیزی آنها را بعدا یکی نمیکند.- حد ۱۲۸ بایت است، نه ۱۲۸ حرف. هر حرف فارسی در UTF-8 دو بایت است، پس یک نام فارسی حداکثر حدود ۶۴ حرف میتواند باشد.
قاعدهای که پیشنهاد میکنیم و بقیه مستندات با آن نوشته شده: حروف کوچک انگلیسی، snake_case، فعل در زمان گذشته، شیء قبل از فعل. order_completed نه completeOrder، product_viewed نه View Product. دلیلش زیبایی نیست: نام رویداد در پنل، در سگمنتساز، در تریگر سناریو و در خروجی CSV عینا همان رشتهای است که فرستادهاید، و یک فهرست با سه شکل نوشتار قاطی، فهرستی است که کسی نمیتواند از آن انتخاب کند.
سرور هیچ محدودیتی روی تعداد نامهای متمایز شما نمیگذارد. این را جدی بگیرید، چون بخش بعد درباره همان است.
اگر نامها در یکپارچهسازی شما از قبل چیز دیگری است، chk_out_v2 یا SUB_RENEW، برای خوانا شدن پنل لازم نیست چیزی را که کدتان میفرستد عوض کنید. پنل برای هر رویداد یک نام نمایشی نگه میدارد که فقط از «مدیریت داده»، در دستهٔ «رویدادها» گذاشته میشود. فقط نام نمایشی است: آنچه SDK میفرستد، آنچه تعریف یک سگمنت به آن ارجاع میدهد و آنچه در خروجی میآید هیچکدام عوض نمیشوند، پس تغییر نام نمیتواند سگمنت یا قیف ذخیرهشدهای را خراب کند. گذاشتنش به مجوز settings.write نیاز دارد.
وقتی نام رویداد یک آدرس است
نام رویداد را از یک متغیر نسازید. این جمله انتزاعی به نظر میرسد تا وقتی که ببینید چطور اتفاق میافتد.
متد page(name) در SDK وب، رویداد را دقیقا به همان رشتهای نامگذاری میکند که به آن دادهاید. یکی از مشتریها به آن pathname + search را داد. نتیجه: نام رویداد کل آدرس شد، و هر کد معرف و هر شناسه پروفایل یک نوع رویداد جدید ساخت. عددهای واقعی آن حساب:
- ۱۶۹ نام متمایز آدرسشکل روی ۵۶۱۸ سطر.
- فهرست رویدادها در یک روز به ۱۶۸ نام رسید: ۱۵۳ تای آن آدرس بود و ۱۵ تا رویداد واقعی.
- «صفحه بازیها را باز کرد» بین ۴۶ نام پخش شده بود:
/games/و/games/?ref=GPE9UHTVو/games/?utm_source=ecrm&utm_medium=inapp_bannerو/u/69235/و بقیه. - همه آن ۱۶۹ نام، وقتی به مسیر تبدیل شوند، ۲۳ تا میشوند.
چرا این فقط زشت نیست بلکه داده را از دست میدهد: فهرست رویدادها (GET /v1/schema/events) حداکثر ۵۰۰ نام برمیگرداند، مرتبشده بر اساس حجم نزولی، فقط از ۹۰ روز اخیر. تنانتی که در این وضعیت است، نامهای واقعیاش از ته فهرست میافتند بیرون. بازاریاب نمیتواند سگمنت «خرید کرد» را بسازد چون نام رویدادش در فهرست نیست.
راه درست: صفحه را به نام مسیر بفرستید نه به نام آدرس، و آدرس را همانجا که جایش هست بگذارید. اگر autoContext روشن باشد، SDK وب خودش context.page.url و context.page.path و context.page.search را روی هر پیام میگذارد و کلکتور دو تای اول را در ستونهای page_url و page_path ذخیره میکند. پس آدرس دقیق از دست نمیرود.
import { init, page } from "@segmentic/web";
init({
writeKey: "wk_seg_...",
apiHost: "https://in.segmentic.net",
// Off, because the automatic page view fires on init and would land
// beside the route-named one below.
autoPageView: false,
});
function routeNameOf(pathname) {
return pathname
.replace(/\/$/, "")
.replace(/\/u\/\d+/g, "/u/:id")
.replace(/\/(\d+)(?=\/|$)/g, "/:id") || "/";
}
// On the first load, and again on every client-side navigation.
page(routeNameOf(location.pathname));
یک نکته درباره autoPageView که تایپ خود SDK آن را جور دیگری میگوید. توضیح تایپ میگوید «یک بازدید صفحه روی راهاندازی و روی جابهجایی در تاریخچه». کد فقط روی راهاندازی میفرستد. تنها شنوندههایی که SDK وب نصب میکند visibilitychange و pagehide و online هستند و هیچجای سورس هیچ popstate یا وصلهای روی pushState وجود ندارد. یعنی یک اپ تکصفحهای باید خودش روی هر جابهجایی page() را صدا بزند، وگرنه کل نشست کاربر یک بازدید صفحه ثبت میکند.
و اگر همین حالا در این وضعیت هستید: اسکریپت deploy/scripts/rename-url-event-names.sh روی برنچ chore/rename-the-url-shaped-event-names این کار را برای سطرهای موجود میکند. رشته پرسوجو را حذف میکند، اسلش انتهایی را حذف میکند، چند مسیر پویای نامبرده را جایگزین میکند و بعد هر بخش عددی باقیمانده را به :id تبدیل میکند. بهصورت پیشفرض فقط گزارش میدهد و با --apply اجرا میشود.
این برنچ در main مرج نشده است، پس اسکریپت روی نصب استاندارد در دسترس نیست. مهمتر: کاری که میکند یک ALTER TABLE ... UPDATE است و برگشت ندارد، چون مقدار قبلی جایی نگه داشته نمیشود. خود اسکریپت پیش از --apply میشمارد که چند سطر page_url خالی دارند و اگر حتی یکی باشد اجرا نمیشود، چون برای آن سطرها آدرس فقط در نام زندگی میکند.
انتخاب ویژگیها
روی رویداد آن چیزی را بگذارید که در همان لحظه درست بود. مبلغی که پرداخت شد، کدی که تخفیف داد، دستهای که محصول در آن بود. اگر بعدا قیمت محصول عوض شود، رویداد دیروز هنوز قیمت دیروز را نشان میدهد، و این تنها راهی است که «درآمد ماه گذشته» معنی درستی داشته باشد.
سه چیز را روی رویداد نگذارید:
- چیزی که کلاینت خودش میفرستد.
contextهمین حالا دستگاه، سیستمعامل، نسخه اپ، صفحه، کمپین، زبان و منطقه زمانی را میآورد. ویژگیos_nameروی رویداد فقط یک نسخه دوم و بدتر از ستونی است که وجود دارد. - توکن، رمز و شماره کارت. ویژگیها در صفحه تایملاین کاربر عینا به تیم پشتیبانی نشان داده میشوند و در خروجی CSV هم میآیند.
- چیزی که فقط یک مقدار برای هر آدم دارد و تغییر میکند، مثل «سطح باشگاه مشتریان». آن ویژگی پرونده است.
کلید ویژگی هم نرمال میشود، و بدانید چطور، چون کلید چیزی است که بعدا در فیلتر تایپ میکنید:
| ورودی | کلید ذخیرهشده |
|---|---|
" spaced key " | spaced_key |
"dotted.key" | dotted_key |
"dashed-key" | dashed_key |
"multi space" | multi_space |
"_leading_" | leading |
"" یا " " | حذف میشود |
یعنی: فاصله و نقطه و خط تیره به یک _ تبدیل میشوند، _ ابتدا و انتها حذف میشود، کاراکتر کنترلی دور ریخته میشود و کلید در ۱۲۸ بایت بریده میشود. اما کلیدها کوچک نمیشوند و نرمالسازی فارسی روی آنها اجرا نمیشود. Price و price دو ویژگی جدا هستند.
یک ویژگی چه چیزی میتواند نگه دارد
ClickHouse نوع Map همگن دارد، پس هر ویژگی در یکی از دو نقشه یا هر دو نوشته میشود: props_str برای متن و props_num برای عدد. جدول کامل:
| مقدار در JSON | در props_str | در props_num |
|---|---|---|
null | نوشته نمیشود | نوشته نمیشود |
| رشته | نرمالسازی فارسی، بریده در ۸۱۹۲ بایت | نوشته نمیشود |
true یا false | "true" یا "false" | 1 یا 0 |
| عدد | شکل متنی عدد | خود عدد |
| آرایه یا شیء | همان JSON بهصورت رشته، بریده در ۸۱۹۲ بایت | نوشته نمیشود |
| چیزی که JSON نمیشود | نوشته نمیشود | نوشته نمیشود، بهعلاوه هشدار unserialisable_property |
null یعنی «تنظیم نشده»، و اگر آن را "" ذخیره میکردیم فیلتر «تنظیم نشده» غلط جواب میداد.
عدد صحیح، صفر اضافه نمیگیرد: 1234 بهصورت رشته "1234" ذخیره میشود نه "1234.0". بدون این، شماره سفارشی که بهصورت عدد JSON میآید، دیگر به سیستم خود مشتری join نمیشد.
{
"type": "track",
"user_id": "u_88123",
"event": "order_completed",
"properties": {
"str": "hello",
"num": 42.5,
"int_like": 1234,
"bool_t": true,
"bool_f": false,
"nil": null,
"arr": [1, 2],
"obj": { "a": 1 }
}
}
props_str = { str: "hello", num: "42.5", int_like: "1234",
bool_t: "true", bool_f: "false",
arr: "[1,2]", obj: "{\"a\":1}" }
props_num = { num: 42.5, int_like: 1234, bool_t: 1, bool_f: 0 }
nil در هیچکدام نیست. arr و obj فقط متناند، ولی با توابع JSON خود ClickHouse هنوز قابل فیلتر شدناند. هیچچیزی تخت نمیشود و هیچ عمقی از دست نمیرود.
مهمترین نتیجه این جدول: رشته عددشکل هرگز عدد نمیشود. اگر "price": "2450000" بفرستید، props_num هیچوقت price نمیگیرد و فیلتر «بیشتر از» روی آن هرگز چیزی پیدا نمیکند. این عمدی است، چون تجزیهکردن "01234" صفر ابتدایی کد پستی را دور میریزد. اگر منظورتان عدد است، در JSON عدد بفرستید. همین قاعده برای رقم فارسی هم هست: "۲۴۵۰۰۰۰" یک رشته است و رشته میماند.
هیچ ثبت نوعی وجود ندارد. هر رویداد مستقل نرمال میشود و هیچجای مسیر ورودی حافظهای از این ندارد که این کلید دفعه قبل چه نوعی بود. پس فرستادن price بهصورت عدد و بعد بهصورت متن، نه خطا میدهد نه هشدار. فقط بعضی سطرها در props_num هستند و بعضی نیستند.
حدها و اینکه وقتی رد میشوید چه میشود
| چیز | حد | وقتی رد شود |
|---|---|---|
| ویژگی در هر رویداد | ۲۵۶ | ۲۵۶ تای اول نگه داشته میشود، بقیه دور ریخته، هشدار too_many_properties |
| طول کلید ویژگی | ۱۲۸ بایت | بریده میشود |
| طول مقدار ویژگی | ۸۱۹۲ بایت | بریده میشود، روی مرز حرف UTF-8 |
| ویژگی پرونده در هر پیام | ۲۵۶ | حلقه متوقف میشود، هشدار too_many_traits |
| آیتم در هر batch | ۵۰۰ | کل درخواست با batch_too_large رد میشود |
| اندازه بدنه | ۵ مگابایت | کد ۴۱۳ |
| طول نام رویداد | ۱۲۸ بایت | رد با event_name_too_long |
طول user_id و anonymous_id و message_id | ۲۵۶ بایت | رد با id_too_long |
طول session_id | ۲۵۶ بایت | بریده میشود، رد نمیشود |
طول previous_id روی alias | حدی ندارد | هیچ بررسی طولی روی آن نیست؛ فقط سقف ۵ مگابایتی بدنه محدودش میکند |
| آدرس و مسیر و ارجاع صفحه | ۲۰۴۸ بایت | بریده میشود |
| عمق تودرتویی | حدی ندارد | آرایه و شیء یکجا به JSON تبدیل میشوند |
دو ریزهکاری که فقط وقتی به آنها برمیخورید که دیر شده است:
«۲۵۶ تای اول» ترتیب مشخصی ندارد. پیمایش نقشه در Go تصادفی است، پس اگر ۳۰۰ ویژگی بفرستید، اینکه کدام ۴۴ تا حذف میشوند در هر رویداد فرق میکند. نتیجه یک ستون نیمهپر است که در گزارش شبیه داده کمکیفیت به نظر میرسد نه شبیه یک باگ. متن هشدار عدد دقیق را میگوید: 300 properties sent, keeping 256.
یک ویژگی با مقدار null یکی از ۲۵۶ جا را میگیرد ولی چیزی ذخیره نمیکند. برای ویژگیهای پرونده اینطور نیست: آنجا مقدار خالی قبل از شمارش رد میشود.
بریدن هرگز وسط یک حرف اتفاق نمیافتد. اگر میافتاد، رشته UTF-8 نامعتبر میشد، ClickHouse کل دسته را رد میکرد، و یک رشته خراب کل یک پارتیشن را متوقف میکرد.
درآمد، و اینکه چطور بیاجازه جمع میشود
اینجا یک رفتار هست که باید قبل از طراحی رویدادهایتان بدانید: درآمد از هر رویدادی استخراج میشود، نه فقط از سفارش.
استخراج به این ترتیب کار میکند، اولین مقدار غیرصفر برنده است:
props_num["revenue"]props_num["total"]props_num["value"]- و اگر هیچکدام نبود:
props_num["price"] × props_num["quantity"]، باquantityپیشفرض ۱ وقتی نیامده یا مثبت نیست.
ارز از props_str["currency"] میآید، بزرگ میشود و در ۸ بایت بریده میشود؛ پیشفرض IRR است. IRT هم یک مقدار شناختهشده است، اما هیچ تبدیلی بین ریال و تومان انجام نمیشود. هر عددی که بفرستید در همان واحدی میماند که گفتهاید و جمعها فرض میکنند شما ثابت ماندهاید.
حالا قسمت گران: پرونده کاربر درآمد را از هر رویدادی که آن را حمل کند جمع میکند.
| نام رویداد | اثر روی پرونده |
|---|---|
order_completed | total_revenue زیاد میشود، order_count یکی بالا میرود، last_order_at جلو میرود |
order_refunded و order_cancelled | قدرمطلق مبلغ از total_revenue کم میشود (کف صفر) و order_count یکی پایین میآید (کف صفر) |
| هر نام دیگری | مبلغ به total_revenue اضافه میشود |
پس یک رویداد product_added_to_cart که price و quantity دارد، همین حالا به ارزش طول عمر آن کاربر اضافه میکند، بدون اینکه کسی سفارشی ثبت کرده باشد. سگمنت «مشتری ویآیپی» که روی total_revenue ساخته شده، پر میشود از آدمهایی که فقط سبد پر کردهاند.
کلیدهای revenue و total و value و همچنین جفت price با quantity را فقط روی رویدادی بگذارید که واقعا پول جابهجا شده است. برای بقیه اسم دیگری بگذارید: unit_price، cart_total، estimated_value. این کلیدها رزرو شدهاند و کد به آنها معنی میدهد.
order_id و product_id هم در فهرست کلیدهای رزرو اعلام شدهاند، ولی هیچ کدی آنها را نمیخواند. ویژگی معمولیاند.
نامهایی که پلتفرم به آنها معنی میدهد
پلتفرم یازده نام استاندارد فروشگاهی را اعلام میکند. فقط سهتای آنها رفتار دارند (بخش بالا) و بقیه نام معمولیاند که فقط در پنل برچسب فارسی میگیرند:
product_viewed و product_added_to_cart و product_removed_from_cart و cart_viewed و checkout_started و order_completed و order_refunded و order_cancelled و searched و signed_up و signed_in.
«فهرست پیشفرض» را بیش از آنچه هست حساب نکنید. سگمنتساز تا وقتی اسکیمای واقعی تنانت بارگذاری نشده، ده نام ثابت را نشان میدهد و بس: order_completed و product_viewed و product_added_to_cart و checkout_started و order_refunded و searched و app_opened و signed_up و message_opened و message_clicked. بقیه نامهای استاندارد، از جمله cart_viewed و product_removed_from_cart و signed_in و order_cancelled، در آن فهرست موقت نیستند. بهمحض اینکه اسکیما برسد، فهرست از رویدادهای واقعی خودتان ساخته میشود و این دهتا فقط برچسب میدهند.
چهار نام را خود سرور میسازد و شما نمیتوانید عوضشان کنید: page_viewed و screen_viewed وقتی page یا screen بدون نام بیاید، و identify و alias که همیشه ثابتاند.
چند نام را هم ورکرهای خود سگمنتیک داخل جریان رویدادهای شما مینویسند، تا تعامل با کمپین دقیقا با همان ابزاری قابل پرسوجو باشد که رویدادهای اپ. این نامها را خودتان نفرستید:
| نام | چه کسی مینویسد |
|---|---|
message_sent | فرستنده پیام، روی ارسال موفق |
message_failed | فرستنده پیام، روی شکست |
message_withheld | فرستنده پیام، فقط برای گروه کنترل |
message_opened | نقطه ردیابی باز شدن پیام |
message_withheld توضیح خودش را میخواهد: کسی برای کمپین انتخاب شده و عمدا برایش پیامی نرفته است. سطر نوشته میشود تا سنجش اثر یک خط پایه داشته باشد، چون بدون سطری که بگوید «این آدم را انتخاب کردیم و ساکت ماندیم»، چیزی برای مقایسه با گروه درمان وجود ندارد. هر نوع دیگری از سرکوب پیام عمدا در جریان نمیآید، چون ثبتکردنشان بهعنوان ارسال، دسترسی هر کمپین را دقیقا به اندازه آدمهایی که به آنها نرسیده متورم میکند.
نامهای message_delivered و message_bounced و unsubscribed بهعنوان ثابت اعلام شدهاند و هیچ جای دیگر بکاند از آنها استفاده نمیکند: نه ورکری آنها را مینویسد و نه چیزی آنها را میخواند، پس گزارشی که روی message_delivered بسازید خالی برمیگردد. با این حال خود پنل رویداد تحویل میخواهد. قالب داشبورد «عملکرد پیامرسانی» و متریکهای پایه «نرخ تحویل پیام» و «نرخ بازشدن پیام» و «نرخ کلیک پیام» هرکدام به یک رویداد تحویل نیاز دارند که از اسکیمای رویداد خودتان به آنها وصل شود، و تا اسمی ندهید فعالسازی جلو نمیرود، پس اگر خودتان رویداد تحویل نفرستید، آن داشبورد و آن سه متریک هرگز ساخته نمیشوند.
آنچه کم است رویداد است، نه داده. گزارش کمپین شمار پیامهای تحویلشده خودش را از رسیدهایی میگیرد که مسیر ارسال پس میفرستد، سرویس تحویلپذیری برگشتیها را از مسیر بازگشت SMTP میخواند و روی فهرست مسدود مینویسد، و لغو اشتراک روی پرونده آن آدم در مرکز تنظیمات مینشیند و در ارسال بعدی بهعنوان دلیل سرکوب برمیگردد. هیچکدام اینها بهشکل سطر وارد جریان رویداد شما نمیشود.
ورکر انتساب دقیقا چهار نام را میشناسد، message_sent و message_withheld و message_opened و message_clicked، و هر رویداد دیگری را که به دستش برسد نادیده میگیرد. message_clicked را هم ورکر نمینویسد: SDK وب آن را وقتی میفرستد که بازدیدکننده روی لینکی با sg_mid وارد سایت شود، یک بار، تا وقتی شناسه پیام دیگری جایش را بگیرد. مقایسه با همان یک شناسه داخل حافظه است نه با تاریخچهای از شناسهها، پس بازدیدکنندهای که روی یک پیام بیاید، بعد روی پیام دوم، و بعد دوباره روی همان پیام اول، کلیک پیام اول را دو بار گزارش میکند.
شش نام app_installed و app_opened و app_updated و app_removed و session_started و session_ended در کد بهعنوان «رویدادهای چرخه عمر که SDKها خودکار میفرستند» اعلام شدهاند. هیچ SDKای آنها را نمیفرستد. نه SDK وب، نه اندروید، نه iOS. جستوجو در هر سه، صفر فرستنده پیدا میکند. app_opened در فهرست پیشفرض رویدادهای پنل هم هست و برچسب فارسی دارد، که باعث میشود خودکار به نظر برسد. اگر این رویدادها را میخواهید، خودتان باید بفرستیدشان.
ویژگی رویداد یا ویژگی پرونده
این جایی است که بیشترین اشتباه اتفاق میافتد، و اشتباهش تا ماهها بعد خودش را نشان نمیدهد.
| ویژگی رویداد | ویژگی پرونده | |
|---|---|---|
| چطور فرستاده میشود | properties روی track یا page یا screen | traits روی identify |
| کجا مینشیند | روی همان یک سطر رویداد | روی پرونده کاربر، یک مقدار برای هر آدم |
| تاریخچه دارد | بله، هر رویداد نسخه خودش را نگه میدارد | خیر، آخرین مقدار جای قبلی را میگیرد |
| با کاربر مهمان کار میکند | بله | خیر، تا وقتی user_id نداشته باشید پروندهای ساخته نمیشود |
| برای چه فیلتری خوب است | «کسی که در ۳۰ روز گذشته سفارش بالای ۵۰۰ هزار داشت» | «کسی که الان سطح طلایی است» |
مثال دقیق: city بهعنوان ویژگی روی order_completed یعنی «این سفارش به تهران رفت». همان city بهعنوان ویژگی پرونده یعنی «این آدم الان در تهران زندگی میکند». اولی هرگز عوض نمیشود، دومی با هر identify عوض میشود و مقدار قبلی برای همیشه میرود.
سه چیز از این تفاوت درمیآید که در ساخت سگمنت به آنها میخورید:
ویژگیهای پرونده شناختهشده از نقشه ویژگیها بیرون کشیده میشوند. email و phone و first_name و last_name و gender و birthday و national_id و city و region و country و language و timezone و push_opt_in و email_opt_in و sms_opt_in به ستونهای واقعی پرونده میروند و در نقشه آزاد traits نمیمانند. پس در پاسخ GET /v1/schema/traits دیده نمیشوند. ویژگی email شما گم نشده است، فقط ستون است.
فیلتر عددی روی ویژگی پرونده، کسی را که آن ویژگی را ندارد انتخاب نمیکند. کامپایلر سگمنت شرط را با mapContains میپیچد. جهت خطا عمدی انتخاب شده است: مخاطبی که بیصدا خالی است یعنی کمپینی که نمیرود و کسی متوجه میشود؛ مخاطبی که بیصدا همه است یعنی کمپینی که برای همه رفته و پس گرفته نمیشود.
فیلتر عددی روی ویژگی رویداد این محافظ را ندارد. فیلتر روی props_num بدون mapContains کامپایل میشود، و نقشه در ClickHouse برای کلید غایب مقدار صفر میدهد. یعنی شرط «قیمت کمتر از ۱۰۰۰۰» رویدادهایی را هم میگیرد که اصلا price نفرستادهاند. اگر ویژگی را روی همه رویدادهای آن نام نمیفرستید، شرط «تنظیم شده» را هم کنارش بگذارید.
بررسی «تنظیم شده» برای ویژگی رویداد روی props_str انجام میشود، که مشکلی ندارد: هر مقدار عددی همیشه در هر دو نقشه نوشته میشود، پس props_str مجموعه بزرگتر است.
متن فارسی
هر رشتهای که کاربر میفرستد پیش از ذخیره از Normalize میگذرد. اینها جاهایی است که اجرا میشود:
- نام رویداد
- مقدار هر ویژگی رشتهای
- مقدار هر ویژگی پرونده سفارشی
context.location.countryو.regionو.citycontext.page.title
و اینها جاهایی است که اجرا نمیشود: کلید ویژگی، کلید ویژگی پرونده، user_id، anonymous_id، message_id، session_id، آدرس و مسیر و ارجاع صفحه، مقدارهای UTM. برای email فقط حروف کوچک میشود، phone جدا تجزیه میشود و national_id فقط رقمهایش به لاتین تبدیل میشود.
آنچه انجام میدهد:
| از | به |
|---|---|
یای عربی ي، الف مقصوره ى، ے | یای فارسی ی |
کاف عربی ك، ڪ | کاف فارسی ک |
ة، ۀ | ه |
ؤ | و |
أ، إ، ٱ | ا |
بهعلاوه اعراب و کشیده و ZWJ بهکلی حذف میشوند، و هر دنباله فاصله (شامل فاصله بدونشکست، فاصلههای یونیکد، و BOM) به یک فاصله ساده تبدیل و از دو سر بریده میشود. کپی از Word و از ویرایشگرهای راستبهچپ بیشتر اینها را با خودش میآورد.
آنچه عمدا دستنخورده میماند:
- نیمفاصله.
میرودبرای خواننده کلمه دیگری است غیر ازمیرود. - بزرگی و کوچکی حروف لاتین.
DigikalaهمانDigikalaمیماند. - رقم فارسی.
۱۴۰۵همان۱۴۰۵میماند و به لاتین تبدیل نمیشود. - فاصله با عرض صفر، الف با کلاه، ی با همزه، و همزه تنها.
نتیجه عملی: تهراني که با صفحهکلید عربی تایپ شده و تهرانی که با صفحهکلید فارسی تایپ شده، یک مقدار میشوند و در یک سگمنت میافتند. اما Tehran و tehran یکی نمیشوند، و ۱۲۳ هرگز 123 نمیشود.
تلفن، کد ملی و جنسیت
چهار ویژگی پرونده رفتار مخصوص دارند، چون هویت آدماند و ذخیرهکردنشان به هر شکلی که رسیده، تضمین میکند برای یک انسان دو پرونده داشته باشید.
phone با ParsePhone تجزیه میشود و به شکل E.164 ذخیره میشود، بهعلاوه یک ویژگی مشتق به نام phone_operator. الگوریتم: رقمهای فارسی و عربی به لاتین، بعد فقط رقمها و یک + ابتدایی نگه داشته میشوند، بعد پیشوند کشور برداشته میشود، و آنچه میماند باید دقیقا ده رقم باشد که با ۹ شروع میشود.
09123456789 9123456789 +989123456789 00989123456789
989123456789 0912 345 6789 0912-345-6789 (0912) 345 6789
۰۹۱۲۳۴۵۶۷۸۹ ٠٩١٢٣٤٥٦٧٨٩
نتیجه: phone = "+989123456789" phone_operator = "mci"
"" "abc" "0812345678" "091234567" "091234567890"
"+981234567890" "12345" "+1234567890" "0000000000"
اگر تجزیه شکست بخورد، مقدار خام همانطور که آمده ذخیره میشود و هشدار invalid_phone برمیگردد؛ phone_operator نوشته نمیشود. دور نمیریزیمش، چون یک شماره بدشکل هنوز تنها راه تماس با آن آدم است.
اپراتور از چهار رقم اول شکل داخلی شماره درمیآید، یعنی همان 09XX:
| اپراتور | پیشوندها |
|---|---|
mci (همراه اول) | ۰۹۱۰ تا ۰۹۱۹، ۰۹۹۰ تا ۰۹۹۷، ۰۹۹۹ |
irancell (ایرانسل) | ۰۹۰۰ تا ۰۹۰۵، ۰۹۳۰، ۰۹۳۳، ۰۹۳۵ تا ۰۹۳۹، ۰۹۴۱ |
rightel (رایتل) | ۰۹۲۰ تا ۰۹۲۳ |
shatel (شاتل موبایل) | ۰۹۹۸ |
samantel (سامانتل) | ۰۹۳۱ |
unknown | هر چیز دیگری، مثلا ۰۹۰۶ یا ۰۹۳۲ یا ۰۹۳۴ |
national_id با الگوریتم رقم کنترلی مبنای یازده بررسی میشود. طول باید بین ۸ تا ۱۰ باشد و کوتاهتر از ۱۰ از چپ با صفر پر میشود، چون صفر ابتدایی مرتب در فایل اکسل گم میشود. این پرکردن فقط برای حساب رقم کنترلی است. چیزی که ذخیره میشود همان مقداری است که فرستادید و فقط رقمهایش به لاتین تبدیل میشود، پس 12345679 هشتکاراکتری میماند. کد ملی با رقمهای یکسان مثل 1111111111 رد میشود، حتی وقتی از رقم کنترلی رد شود. اگر نامعتبر باشد، ویژگی کلا حذف میشود و هشدار invalid_national_id برمیگردد. تلفن نامعتبر همانطور که فرستادید نگه داشته میشود، ولی کد ملی نامعتبر اصلا نگه داشته نمیشود، چون مشتریهای سازمانی CRM خودشان را روی این کلید میبندند و یک مقدار غلط، یک پرونده تقلبی میسازد.
email فقط TrimSpace و کوچک میشود. هیچ بررسی قالبی در ورودی انجام نمیشود.
gender به دقیقا یکی از سه مقدار male یا female یا other نگاشت میشود. male از m و male و man و مرد و اقا و پسر میآید؛ female از f و female و woman و زن و خانم و دختر؛ هر چیز دیگری other میشود. مقایسه از تابع Fold میگذرد، پس آقا هم به male میرسد.
بقیه ویژگیهای پرونده، از جمله city و first_name و birthday و language و کلیدهای رضایت، فقط نرمالسازی فارسی و بریدن میگیرند.
محافظ کاردینالیتی
سه چیز را باید از هم جدا نگه داشت، چون فقط یکی از آنها واقعا محافظ دارد.
آنچه محافظت میشود: برچسب متریکها. وقتی رویدادی رد میشود، آنچه بهعنوان دلیل شمرده میشود یکی از کدهای ثابت است، نه متن کامل خطا. متن کامل خطا نوع رویداد یا زمان فرستادهشده توسط شما را در خودش دارد، و پیشتر همان متن مستقیم برچسب Prometheus میشد: یعنی هرکسی که یک کلید نوشتن داشت میتوانست با فرستادن یک نام تصادفی در هر درخواست، به ازای هر درخواست یک سری زمانی جدید بسازد. این حافظه بیکران است و اول از همه سیستم پایش را از کار میاندازد، یعنی دقیقا همان چیزی که برای فهمیدن ماجرا لازم دارید. حالا ۳۰۰ نوع نامعتبر متمایز در یک دسته، به یک برچسب جمع میشوند. بدنه پاسخ همچنان متن کامل و مقدار مقصر را به شما میدهد.
آنچه محافظت نمیشود: تعداد نامهای رویداد شما. هیچ شمارندهای وجود ندارد و هیچ سقفی اعمال نمیشود. تنها اثری که میبینید این است که فهرست رویدادها در ۵۰۰ نام قطع میشود.
آنچه محافظت نمیشود: تعداد مقدارهای متمایز یک ویژگی. هیچ سقفی روی آن هم نیست. اگر شناسه جلسه یا شماره سفارش را بهعنوان مقدار ویژگی بفرستید، این کار درست است و مشکلی ندارد. اگر همان را بهعنوان نام رویداد بفرستید، بخش نام آدرسشکل را دوباره بخوانید.
دیدن اینکه واقعا چه رسید
سه راه، و هر سه چیز متفاوتی به شما میگویند.
بدنه پاسخ. هشدارها همان لحظه برمیگردند. کل فهرست کدهای هشدار: generated_message_id، timestamp_in_future، timestamp_too_old، too_many_properties، unserialisable_property، too_many_traits، invalid_phone، invalid_national_id. در /v1/batch کلکتور وقتی از قبل ۵۰ هشدار جمع کرده باشد، دیگر هشداری اضافه نمیکند، و این بررسی را پیش از افزودن هشدارهای یک آیتم کامل انجام میدهد، پس پاسخ میتواند ۴۹ هشدار بهعلاوه هرچه آیتم بعدی ساخته را حمل کند. شمارشها دقیق میمانند.
{"status":"ok","accepted":2,"rejected":1,"errors":[{"index":1,"reason":"missing_event_name"}]}
یک آیتم بد کل دسته را غرق نمیکند. آیتمهای رد شده با شمارهشان در آرایه گزارش میشوند. توجه کنید که accepted شامل تکراریها هم هست، تا SDK دست از تلاش دوباره بردارد.
فهرست رویدادها. GET /v1/schema/events روی api.segmentic.net با کلید API و دسترسی event.read:
curl -H "Authorization: Bearer sk_seg_..." \
https://api.segmentic.net/v1/schema/events
{
"events": [
{ "name": "product_viewed", "volume": 88000000, "prop_keys": ["product_id","price"], "last_seen": "2026-08-06" },
{ "name": "order_completed", "volume": 4200000, "prop_keys": ["revenue","order_id"], "last_seen": "2026-08-07" }
]
}
last_seen مفیدترین ستون این جدول است: رویدادی با حجم بزرگ و آخرین مشاهده سه هفته پیش، یعنی یکپارچگیای که شکسته است، و هیچ عدد دیگری این را نمیگوید چون حجم تا یک ماه بعد هنوز سالم به نظر میرسد. پنجره ۹۰ روز است، سقف ۵۰۰ نام مرتب بر اساس حجم، و prop_keys اتحاد کلیدهای دو نقشه است بدون هیچ اطلاعاتی از نوع. یعنی این endpoint نمیتواند به شما بگوید نوع یک ویژگی عوض شده است.
تازگی داده به چرخه نوشتن ingestor بند است: هر ۱۰۰۰۰ رویداد یا هر ۵ ثانیه، هرکدام زودتر رسید. کش و job جداگانهای در کار نیست.
دیباگر زنده. در پنل، ضبط را روشن میکنید و ۳۰ دقیقه رویدادها را همانطور که میرسند میبینید، با هشدارهایشان. آخرین ۲۰۰ رویداد نگه داشته میشود. تا وقتی کسی دیباگر را باز نکرده باشد هیچ ضبطی انجام نمیشود.
دیباگر فقط مسیرهای تکرویدادی را ضبط میکند. POST /v1/batch هیچچیزی به آن نمیدهد، و هر سه SDK دستهای میفرستند. یعنی وقتی SDK را تازه نصب کردهاید و رویدادها هم دارند میرسند، دیباگر خالی میماند و شما فکر میکنید کار نکرده است. برای تایید یک نصب SDK از صفحه «اتصال» در پنل استفاده کنید که فعالیت اپ را در ۲۴ ساعت گذشته میپرسد، یا موقتا با POST /v1/track یک رویداد تکی بفرستید.
جدول segmentic.ingest_warnings در اسکیما وجود دارد و توضیحش میگوید تاریخچه هشدارهای هر تنانت را نگه میدارد. هیچچیزی در آن نمینویسد. پس تاریخچه هشدار وجود ندارد: هشدار فقط در بدنه پاسخ و در دیباگر زنده دیده میشود. اگر میخواهید بدانید SDK اندروید شما از دیروز user_id نمیفرستد، باید خودتان پاسخها را لاگ کنید.
تصمیمهایی که بعدا برگشت ندارند
هر ردیف این جدول، چیزی است که یک بار روی داده نوشته میشود و بعد فقط میشود از آن به بعد را درست کرد.
| تصمیم | چرا برگشت ندارد | بهجایش چه کنید |
|---|---|---|
| نام رویداد | تغییر نام یعنی ALTER TABLE ... UPDATE روی سطرهای موجود، که مقدار قبلی را نگه نمیدارد. اسکریپتش هم در main نیست | قبل از اولین ارسال، فهرست نامها را روی کاغذ بنویسید. فهرست آماده در فرهنگنامه رویدادها هست |
| ویژگیای که نفرستادید | هیچ راهی برای افزودن ویژگی به سطرهای نوشتهشده وجود ندارد | ویژگیهای زمینهای را از روز اول بفرستید، حتی اگر هنوز به آنها نیاز ندارید |
| عدد که بهشکل رشته فرستاده شد | رشته عددشکل هرگز تجزیه نمیشود، و رویدادهای بعدی سطرهای قبلی را درست نمیکنند | در JSON عدد بفرستید. رقم فارسی هم رشته است |
مهاجرت تاریخچه از POST /v1/events | آن مسیر یک پنجره ثابت ۳۰ روزه دارد، هرچه قدیمیتر باشد به لبه پنجره چسبانده میشود، پاسخ ۲۰۲ است و هیچ هشداری در بدنه برنمیگردد. یک سال سفارش بهصورت یک روز غولآسا مینشیند | از مسیر واردکردن استفاده کنید که در حالت backfill رد میکند بهجای اینکه جابهجا کند |
| ویژگی پرونده که نوعش عوض شد | فرستادن "abc" بعد از 428 نیمه متنی را عوض میکند ولی نیمه عددی روی 428 میماند، و از مسیر ورودی راهی برای پاککردنش هم نیست: مقدار رشته خالی پیش از رسیدن به پرونده دور ریخته میشود | نوع ویژگی پرونده را ثابت نگه دارید |
| رویداد اشتباهی که فرستاده شد | endpointی برای حذف یک رویداد وجود ندارد | فقط سیاست نگهداری و TTL چهارصدروزه خود جدول و پاکسازی شخص، سطر را برمیدارند |
دو ریزهکاری روی سطر مهاجرت. پنجره ۳۰ روزه فقط برای POST /v1/events است؛ کلکتور روی in.segmentic.net پنجرهاش سیاست نگهداری خود تنانت است و اگر چیزی را بچسباند، هشدار timestamp_too_old را هم در بدنه پاسخ میدهد. و مسیر واردکردن (POST /v1/import/events) روی API عمومی ثبت نشده است: فقط از پنل در دسترس است، پس مهاجرت تاریخچه کاری است که یک نفر با مرورگر انجام میدهد نه یک اسکریپت با کلید مدیریتی.
و یک محدودیت که سیاست شما نیست، سقف پلتفرم است: جدول رویدادها یک TTL چهارصدروزه دارد. هیچ رویدادی بیشتر از ۴۰۰ روز نمیماند، هر سیاست نگهداریای که تنظیم کرده باشید. اگر به نگهداری بلندتر نیاز دارید، خروجی بگیرید.
قدم بعد: فرهنگنامه رویدادها فهرست آماده برای شش نوع کسبوکار دارد، و هویت میگوید user_id و anonymous_id چطور به هم وصل میشوند.