گزارش و خروجی گرفتن
ساخت قیف و گزارش ماندگاری، و ورود و خروج امن داده از پنل.
روی میزبان مدیریت https://api.segmentic.net دو گزارش وجود دارد و نه بیشتر: قیف و ماندگاری. باقی چیزهایی که در پنل میبینید، مسیر، ریزش، RFM، درگیری، داشبوردساز، هیچکدام آدرس عمومی ندارند. فهرست کامل در آنچه فقط در پنل هست و آنچه ممکن نیست آمده است.
هر دو گزارش با کلید API (sk_seg_...) کار میکنند، نه با کلید نوشتن. کلید نوشتن روی in.segmentic.net است و به این آدرسها دسترسی ندارد.
روی نصب محلی، تا وقتی PUBLIC_API_ADDR را ست نکنید این میزبان اصلاً بالا نمیآید. کالکتور روی http://localhost:8080 جدا کار میکند.
قیف
POST /v1/reports/funnel. دسترسی لازم analytics.read. هزینه ۲۵ واحد از سقف درخواست. مهلت اجرا ۴۵ ثانیه. حجم بدنه حداکثر ۱ مگابایت.
عددی که برمیگردد تجمعی است. users در هر مرحله یعنی هر کسی که به این مرحله یا جلوتر رسیده، نه هر کسی که دقیقاً همینجا ایستاده. ClickHouse با windowFunnel دورترین مرحلهای را که هر کاربر رسیده گزارش میکند و ما هیستوگرام را از انتها جمع میزنیم. اگر مستقیم بخوانیدش، قیفی میسازید که مرحلههای بعدیاش کاربر بیشتری از مرحلههای اول دارند.
curl -X POST https://api.segmentic.net/v1/reports/funnel \
-H "Authorization: Bearer sk_seg_..." \
-H "Content-Type: application/json" \
-d '{
"steps": [
{"name": "product_viewed", "label": "دیدن محصول"},
{"name": "add_to_cart"},
{"name": "purchase"}
],
"range": {"from": "2026-05-01T00:00:00Z", "to": "2026-06-01T00:00:00Z"},
"window": "7d"
}'
| فیلد | نوع | اجباری | پیشفرض | قاعده |
|---|---|---|---|---|
steps | آرایه | بله | ندارد | بین ۲ تا ۱۲ عضو |
steps[].name | رشته | بله | ندارد | نام رویداد، حداکثر ۲۵۶ بایت |
steps[].label | رشته | خیر | همان name | فقط برچسب نمودار |
steps[].filters | آرایه | خیر | ندارد | حداکثر ۱۰ عضو |
range.from | RFC3339 | بله | ندارد | شامل خودش |
range.to | RFC3339 | بله | ندارد | شامل خودش نیست |
window | رشته | بله | ندارد | مثل "7d" یا "2h" یا "30m" |
strict | بولی | خیر | false | یعنی هیچ رویداد دیگری بین دو مرحله نباشد |
split_by | رشته | خیر | خالی | فهرست مجاز پایینتر |
from شامل خودش است و to نیست، تا دو بازهی پشتسرهم رویدادهای مرز را دوبار نشمارند. طول بازه بیشتر از ۷۳۰ روز نمیشود.
window را خودمان پارس میکنیم چون Go واحد روز ندارد. "7d" و "1d" و "0.5d" و "2h" و "30m" قبولاند. "" و null صفر میشوند و صفر رد میشود. "tomorrow" خطای خواندن JSON است. پنجرهای بلندتر از خود بازه هم رد میشود: کسی نمیتواند در گزارش سیروزه نود روز طول بکشد تا تبدیل شود.
هیچجای این پلتفرم نام رویداد را اعتبارسنجی نمیکند. یک غلط تایپی در steps[].name قیفی کاملاً درست با اعداد صفر برمیگرداند، که از یک مخاطب واقعی صفرنفره قابل تشخیص نیست. قبل از نوشتن قیف، GET /v1/schema/events را صدا بزنید و ببینید نام واقعاً وجود دارد.
کاربران ناشناس (user_id خالی) و ترافیک ربات از هر قیف، ماندگاری و مسیری بیروناند. رباتها در انبار داده پاک نمیشوند، فقط با is_bot علامت میخورند و گزارشها کنارشان میگذارند: افت ترافیکی که کسی نتواند توضیحش بدهد، اعتماد به کل اعداد را از بین میبرد.
فهرست قیفهای ذخیرهشده
پنل قیفهای نامدار نگه میدارد. یک بار تعریف را ذخیره کنید و در فهرستی مینشیند که هر عضوش نمودار خودش را میکشد، و همین نکتهاش است: بیست قیف کنار هم، همانجایی است که آدم میفهمد یکیشان سهشنبه خراب شده.
هیچکدام از اینها آدرس عمومی ندارد. فهرست روی کنترلپلین پنل است که عمداً از بیرون route نمیشود، پس GET /v1/funnels با کلید مدیریتی در دسترس نیست. این بخش را نوشتهایم تا کسی یک ساعت وقت نگذارد و بعد ۴۰۴ بگیرد. آنچه API میدهد همان POST /v1/reports/funnel این صفحه است، که قیف را از تعریفی که خودتان نگه داشتهاید حساب میکند.
وقتی هنوز قیفی نساختهاید، فهرست فقط یک حالت خالی و دکمهٔ «شروع از قیفهای آماده» نشان میدهد. قیفهای آماده تا وقتی این دکمه را نزنید باز نمیشوند. برای هر نوع کسبوکار یک الگو هست و مرحلههایش همانهایی است که فرهنگنامهٔ رویدادها برای همان صنف منتشر میکند. برداشتن هرکدام یک قیف معمولی میسازد و هیچ چیزی در نتیجه یادش نمیماند که از یک الگو آمده. هر الگو اول با کاتالوگ رویدادهای خود حساب سنجیده میشود و اگر رویدادی را نام ببرد که حساب هرگز نفرستاده، همانجا میگوید؛ چون مرحلهای که کسی نمیفرستد قیفی کاملاً درست با اعداد صفر برمیگرداند و این از یک مخاطب واقعی صفرنفره قابل تشخیص نیست.
دو چیز دربارهی این فهرست ارزش دانستن دارد، حتی اگر فقط با API کار میکنید.
نمودار هر کارت موقع باز شدن صفحه حساب نمیشود. یک کار پسزمینه هر قیف ذخیرهشده را دورهای دوباره حساب میکند و جواب را روی همان سطر مینویسد، چون کشیدن زندهشان یعنی به ازای هر کارت یک windowFunnel روی کل جدول رویدادها، در هر بازدید. پس هر کارت میتواند چند ساعت عقب باشد، و هر کارت تاریخ عددش را مینویسد. برای عدد همین لحظه، قیف را باز کنید و اجرایش کنید.
تعریف ذخیرهشده مرحلهها، پنجرهی تبدیل، حالت سختگیرانه و شکستن را نگه میدارد و بازهی زمانی را نه. بازه، سؤالی است که از یک تعریف ذخیرهشده پرسیده میشود نه بخشی از خودش؛ اگر «سی روز گذشته» در سطر منجمد میشد، هر قیف ذخیرهشده بیصدا پیر میشد و یک سال بعد این فهرست مجموعهای از سؤالهای بهار پارسال بود.
فیلترها
هر فیلتر سه فیلد دارد: {"prop": "...", "op": "...", "value": "..."}. value همیشه رشته است، حتی برای عملگرهای عددی.
| گروه | عملگرها | روی چه ستونی |
|---|---|---|
| متنی | eq، ne، contains، prefix | props_str |
| عددی | gt، gte، lt، lte، num_eq، num_ne | props_num |
عملگر ناشناخته خطای ۴۰۰ میدهد. طول prop حداکثر ۱۲۸ بایت و طول value حداکثر ۵۱۲ بایت است.
یک نکته که وقت آدم را میگیرد: اگر value یک عملگر عددی به عدد اعشاری تبدیل نشود، بهجای خطا بهصورت عدد 0 رندر میشود، یعنی به هیچ سطری نمیخورد. دلیلش این است که نیمهتایپشدن یک عدد در رابط کاربری نباید کل نمودار را با stack trace خالی کند. نتیجهاش برای شما این است که یک غلط تایپی در فیلتر عددی، بیسروصدا قیف خالی میسازد.
{
"steps": [
{"name": "product_viewed",
"filters": [{"prop": "category", "op": "eq", "value": "mobile"}]},
{"name": "purchase",
"filters": [{"prop": "amount", "op": "gte", "value": "500000"}]}
],
"range": {"from": "2026-05-01T00:00:00Z", "to": "2026-06-01T00:00:00Z"},
"window": "2d"
}
شکستن به بخش
split_by یا یکی از این کلیدهای مجاز است، یا شکل prop: بهعلاوهی نام یک ویژگی.
split_by | ستون |
|---|---|
platform | os_name |
os | os_name |
device | device_type |
app_version | app_version |
country | country |
city | city |
region | region |
province | region |
utm_source | utm_source |
utm_campaign | utm_campaign |
browser | browser_name |
prop:category روی props_str گروه میکند، عمداً نه روی props_num: یک ویژگی عددی بهعنوان بعد شکستن، نموداری با چهار هزار میله میسازد. کلید بعد از prop: هیچ اعتبارسنجی ندارد؛ کلید ناشناخته همهچیز را زیر رشتهی خالی جمع میکند، که جواب درست و خواناتری از خطاست.
مقدار ناشناختهای که با prop: شروع نشود، ۴۰۰ میگیرد با متن انگلیسی خام analytics: unknown breakdown "...".
پاسخ قیف
{
"steps": [
{"index": 0, "name": "product_viewed", "label": "دیدن محصول",
"users": 1000, "from_start": 1.0, "from_previous": 1.0, "dropped_here": 0},
{"index": 1, "name": "add_to_cart", "label": "add_to_cart",
"users": 600, "from_start": 0.6, "from_previous": 0.6, "dropped_here": 400},
{"index": 2, "name": "purchase", "label": "purchase",
"users": 300, "from_start": 0.3, "from_previous": 0.5, "dropped_here": 300}
],
"entered": 1000,
"completed": 300,
"conversion": 0.3,
"description": "کاربرانی که «دیدن محصول» سپس ... را به ترتیب انجام دادند، حداکثر در ۷ روز."
}
from_startوfrom_previousوconversionهمه کسری بین صفر و یکاند، نه درصد.from_previousمرحلهی صفر همیشه1است.dropped_hereمرحلهی صفر همیشه صفر است.- تقسیم بر صفر مهار شده: قیف خالی صفر میدهد، نه
NaN. descriptionاز همان درخواستی ساخته میشود که کوئری از آن ساخته شد، پس نمیتواند از اعداد زیرش جدا بیفتد. تاریخها روی این میزبان همیشه جلالیاند، چون این میزبانAccept-Languageرا نمیخواند و زبان پیشفرضش فارسی است.
با split_by، کلید buckets هم اضافه میشود، مرتبشده بر اساس entered نزولی. steps و entered و completed سطح بالا همچنان کل قیف روی همهی بخشها هستند، نه بزرگترین بخش.
{
"steps": [],
"buckets": [
{"value": "ios", "steps": [], "entered": 1000, "completed": 100, "conversion": 0.1},
{"value": "android", "steps": [], "entered": 200, "completed": 100, "conversion": 0.5}
],
"entered": 1200,
"completed": 200,
"conversion": 0.16666666666666666,
"description": "..."
}
اگر تنها کلید بخشبندی رشتهی خالی باشد، buckets اصلاً نمیآید.
ماندگاری
POST /v1/reports/retention. همان دسترسی، همان هزینه ۲۵ واحد، همان مهلت ۴۵ ثانیه.
curl -X POST https://api.segmentic.net/v1/reports/retention \
-H "Authorization: Bearer sk_seg_..." \
-H "Content-Type: application/json" \
-d '{
"start": {"name": "signup"},
"return": {"name": "purchase"},
"range": {"from": "2026-05-01T00:00:00Z", "to": "2026-05-10T00:00:00Z"},
"granularity": "day",
"periods": 3
}'
| فیلد | نوع | اجباری | پیشفرض | قاعده |
|---|---|---|---|---|
start | همان شکل مرحله | خیر | {} | نام خالی یعنی «هر فعالیتی» |
return | همان شکل مرحله | خیر | {} | نام خالی یعنی «هر فعالیتی» |
range | {from, to} | بله | ندارد | مثل قیف، حداکثر ۷۳۰ روز |
granularity | رشته | خیر | day | day یا week یا month |
periods | عدد | خیر | ۳۰ | بین ۱ تا ۶۰ |
start و return دو فیلد جدا هستند چون «برگشت» بهندرت یعنی «همان کار را دوباره کرد». اگر هر دو خالی باشند گزارش روی هر فعالیتی حساب میشود.
periods صفر یا منفی به ۳۰ تبدیل میشود، ولی بیشتر از ۶۰ خطای ۴۰۰ میگیرد با متن انگلیسی analytics: at most 60 periods. جدولی که بیشتر از ۱۲۰ سطر کوهورت بسازد هم رد میشود: یک سال کامل با دانهبندی روز رد میشود، همان بازه با دانهبندی ماه قبول است.
این نقطهی پایانی فیلد event ندارد و کلیدهای ناشناخته را هم رد نمیکند، فقط نادیده میگیرد. اگر {"event": "purchase"} بفرستید، start و return خالی میمانند و جدولی از «هر فعالیتی» میگیرید که هیچجا نمیگوید سوال شما را نفهمیده است. تنها سرنخ، جملهی description است که در آن «هر فعالیتی» نوشته شده. همین اشکال، ابزار آمادهی MCP ما را هم گرفتار میکند؛ صفحهی MCP را ببینید.
کوهورت جلالی
هیچکدام از توابع تقویمی ClickHouse برای این بازار درست نیستند، پس مرز هر سطل در Go و در وقت Asia/Tehran حساب میشود و از ClickHouse فقط پرسیده میشود که هر زمان در کدام سطل میافتد.
| دانهبندی | شروع سطل | گام بعدی |
|---|---|---|
day | نیمهشب تهران | یک روز |
week | شنبه | هفت روز |
month | روز اول ماه جلالی | یک روز بعد از پایان همان ماه جلالی |
toStartOfMonth گرگوری است، پس گزارش «ماهانه» ده روز آنطرفتر از جایی بریده میشد که هر کاربر ایرانی فکر میکند ماه شروع میشود. toStartOfWeek فقط دوشنبه یا یکشنبه میدهد و هفتهی ایرانی شنبه شروع میشود، پس هر هفته میان دو سطر تقسیم میشد.
حساب ماه هم «بهعلاوهی ۳۱ روز» نیست: ماه جلالی ۲۹ یا ۳۰ یا ۳۱ روز است و جمعزدن ۳۱ روز یک ماه سیروزه را کامل رد میکند. مرزها در Go ساخته و بهصورت Array(Date) به ClickHouse داده میشوند.
تاریخهای مرزی بهصورت تاریخ محلی تهران فرستاده میشوند، نه تبدیلشده به UTC. تبدیل به UTC هر مرز را سه ساعت و نیم عقب میبرد و ساعتهای اول هر روز را در سطل روز قبل میگذارد.
کوهورت هر کاربر، اولین دورهای است که در بازهی درخواستشده واجد شرط شده، نه اولین باری که در کل تاریخ دیده شده. اگر اولین همیشگی ملاک بود، مشتری دوسالهای که امروز خرید کرد در کوهورت همین ماه میافتاد و خانهی اول جدول دیگر معنایی نداشت.
خانههایی که هنوز معلوم نیستند
هر خانه یک فیلد observable دارد. false یعنی «گزارش هنوز آنقدر عمر نکرده که بداند»، نه صفر. کوهورتی که دیروز شروع شده عدد روز سیام ندارد، و نمایش آن خانه بهصورت صفر درصد، همان کاری است که یک محصول سالم را در حال مرگ نشان میدهد.
مرز، آخرین سطلی است که کاملاً تمام شده، سنجیده با ساعتی که یکبار در هر درخواست خوانده میشود تا همهی خانههای یک پاسخ با یک لحظه سنجیده شوند. دورهی در جریان در جدول میآید ولی از منحنی میانگین بیرون است.
پاسخ ماندگاری
{
"granularity": "day",
"period_label": "روز",
"cohorts": [
{
"cohort": "2026-05-01",
"label": "۱۱ اردیبهشت ۱۴۰۵",
"size": 100,
"cells": [
{"period": 0, "users": 100, "rate": 1.0, "observable": true},
{"period": 1, "users": 40, "rate": 0.4, "observable": true},
{"period": 2, "users": 25, "rate": 0.25, "observable": true},
{"period": 3, "users": 0, "rate": 0.0, "observable": false}
]
}
],
"average": [
{"period": 0, "users": 10004, "rate": 1.0, "observable": true},
{"period": 1, "users": 1004, "rate": 0.10036, "observable": true}
],
"description": "از کاربرانی که برای اولین بار «signup» انجام دادند ..."
}
cohortکلید ماشینی است و همیشهYYYY-MM-DDگرگوری از لحظهی تهران.labelسرستون خواندنی است و روی این میزبان جلالی با ارقام فارسی میآید.cellsهمیشه دقیقاً یکی بیشتر ازperiodsعضو دارد، از دورهی صفر تا آخر.rateکسری بین صفر و یک است.averageمنحنی وزنی است: مجموع برگشتیها تقسیم بر مجموع شروعکنندهها، فقط روی خانههایobservable. میانگین سادهی درصدهای هر کوهورت نیست، چون کوهورت چهارنفرهای که همه برگشتند، منحنی را بهاندازهی کوهورت چهلهزارنفره بالا میکشید.- کوهورتهایی که هیچ سطری ندارند اصلاً در
cohortsنمیآیند. نتیجهی خالی یعنی"cohorts": [].
هزینه، مهلت و شکل خطا
این دو نقطهی پایانی همان تابعهای داخلی پنلاند که روی میزبان مدیریت هم ثبت شدهاند. نتیجهاش یک ناهمخوانی واقعی است که باید بدانید:
| نوع خرابی | پاکت پاسخ | کد وضعیت |
|---|---|---|
| کلید نبود، نوع کلید غلط بود، کلید منقضی بود | {"error":{"code":"unauthenticated"}} | ۴۰۱ |
| دسترسی نبود | {"error":{"code":"forbidden","need":"analytics.read"}} | ۴۰۳ |
| سقف درخواست تمام شد | {"error":{"code":"budget_exhausted"}} با هدر Retry-After: 60 | ۴۲۹ |
| حساب قفل نرم خورده | {"error":{"code":"account_locked","details":{"reason":"usage_300"}}} | ۴۰۳ |
| JSON خراب بود | {"error":"یک جملهی فارسی"} | ۴۰۰ |
| گزارش نامعتبر بود | {"error":"یک جملهی فارسی","code":"invalid_report"} | ۴۰۰ |
| انبار داده جواب نداد | {"error":"یک جملهی فارسی"} | ۵۰۳ |
کلاینتی که فقط error.code را میخواند، روی هر ۴۰۰ و هر ۵۰۳ این دو نقطهی پایانی میشکند، چون آن پاسخها error را رشته میفرستند نه شیء. هر دو شکل را هندل کنید.
نه خطای اعتبارسنجی متفاوت (مرحلهی کم، مرحلهی زیاد، نام خالی، بازهی بد، بازهی پهن، پنجرهی بد، دانهبندی بد، عملگر بد، عمق زیاد) همگی یک کد دارند: invalid_report. متن جمله فرق میکند، کد فرق نمیکند. خرابی انبار داده اصلاً کد ندارد.
سقف درخواست ۶۰۰ واحد در دقیقه است و روی هر کلید حساب میشود، نه روی حساب. هر گزارش ۲۵ واحد است، پس یک کلید در هر دقیقه ۲۴ گزارش میتواند بگیرد. با PUBLIC_API_BUDGET_PER_MINUTE قابل تغییر است. اگر سنجهی سقف خوانده نشود، درخواست رد میشود نه رها: budget_unavailable با ۵۰۳.
قفل نرم وقتی میافتد که مصرف به سیصد درصد سهمیه برسد یا فاکتوری ۷۵ روز عقب بیفتد. فقط چهار مسیر را میبندد: هر دو گزارش، و ساختن و فهرستکردن خروجی. ورود داده (POST /v1/events) و ارسال پیام عمداً باز میمانند.
مهلت گزارش ۴۵ ثانیه است. توجه کنید که GET /v1/capabilities عدد query_timeout_sec را ۳۰ اعلام میکند و آن عدد مهلت گزارشها نیست. هیچکدام از سقفهای تحلیلی (۱۲ مرحله، ۶۰ دوره، ۱۲۰ کوهورت، ۷۳۰ روز، ۴۵ ثانیه) روی هیچ نقطهی پایانی منتشر نمیشوند. باید در کد خودتان بنویسیدشان.
خروجی گرفتن
خروجی یک کار پسزمینه است، نه یک پاسخ. POST /v1/exports کار را در صف میگذارد و ۲۰۲ برمیگرداند. GET /v1/exports وضعیت را میگوید. هیچ وبهوکی، هیچ فراخوان برگشتی و هیچ اعلانی وجود ندارد؛ تنها راه، پرسیدن دوباره است.
دسترسی لازم data.export است و جداست چون خروجی از ساختمان بیرون میرود: دسترسی خواندن داخل داشبوردی که هر کوئری را لاگ میکند، ریسک دیگری است تا فایل CSV آدرس ایمیل همهی مشتریها روی لپتاپ یک نفر. نقشهای owner و admin و marketer و analyst این دسترسی را دارند؛ viewer و approver و finance ندارند.
ساختن کار خروجی
curl -X POST https://api.segmentic.net/v1/exports \
-H "Authorization: Bearer sk_seg_..." \
-H "Content-Type: application/json" \
-d '{
"kind": "events",
"format": "ndjson",
"spec": {"from": "2026-05-01T00:00:00Z", "to": "2026-06-01T00:00:00Z"}
}'
{"id": 42, "status": "queued", "kind": "events", "expires_after_hours": 168}
kind دقیقاً یکی از این چهارتاست: events، profiles، segment، messages. هر چیز دیگری ۴۲۲ میگیرد با کد export_kind_invalid.
format یا دقیقاً رشتهی csv است، یا هر چیز دیگری که بیسروصدا به ndjson تبدیل میشود. اگر "parquet" بفرستید، ۲۰۲ میگیرید و فایل NDJSON تحویل میگیرید. دلیل پیشفرض بودن NDJSON این است که خروجی رویدادها با ویژگیهای تودرتو مستطیل نیست و صافکردنش در CSV، تودرتویی را بیصدا از بین میبرد.
spec شیء آزادی است که فقط سه کلیدش خوانده میشود:
| کلید | نوع | برای کدام kind | پیشفرض |
|---|---|---|---|
from | رشتهی RFC3339 | events، messages | ۹۰ روز پیش |
to | رشتهی RFC3339 | events، messages | همین حالا |
segment_id | عدد | segment | اجباری؛ صفر یعنی شکست قطعی |
profiles بازه را کلاً نادیده میگیرد. بازهی وارونه بهجای خطا جابهجا میشود. هر چیز غیرقابلخواندنی در spec نادیده گرفته میشود و پیشفرض اعمال میشود.
نصبی که فضای ذخیرهی خروجی نداشته باشد بهجای ۲۰۲ کد ۵۰۳ میدهد. روی چنین نصبی مسیر دانلود اصلاً ثبت نمیشود، پس ۲۰۲ یعنی وعدهی فایلی که هیچجا برای برداشتنش نیست، و کار تا ابد در صف میماند و شبیه کاری در جریان به نظر میرسد. ۵۰۳ و نه ۴۰۰، چون درخواست سالم بوده و نصب سالم نیست، و همین تفاوت است که میگوید کد خودتان را درست کنید یا از مدیر سیستم بخواهید.
فیلد columns وجود ندارد، max_rows وجود ندارد، limit وجود ندارد. ستونها ثابتاند و سقف سطر یک ثابت سروری است.
پیگیری
curl https://api.segmentic.net/v1/exports \
-H "Authorization: Bearer sk_seg_..."
{
"data": [
{
"id": 42, "kind": "events", "format": "ndjson",
"spec": {"from": "2026-05-01T00:00:00Z", "to": "2026-06-01T00:00:00Z"},
"status": "ready", "rows_written": 412334, "bytes": 91203344,
"location": "/var/lib/segmentic/exports/7/42.ndjson",
"attempts": 1, "truncated": false,
"expires_at": "2026-06-08T09:12:00Z",
"requested_by": "api-key:3",
"created_at": "2026-06-01T09:04:00Z",
"finished_at": "2026-06-01T09:12:00Z"
}
],
"has_more": false
}
پنج وضعیت ممکن است: queued، running، ready، failed، expired. هیچ نقطهی پایانی این فهرست را منتشر نمیکند.
locationمسیر فایل روی سرور ماست و روی سیم میآید. برای شما بیمصرف است.attemptsمنتشر میشود چون «شکست خورد» و «سه بار شکست خورد و دست کشید» دو جواب متفاوتاند. سقف تلاش ۳ است.truncatedمیگوید کوئری روی سقف پنج میلیون سطر متوقف شده، نه در انتهای داده، پس فایل همهی نتیجه نیست. وقتی نادرست باشد اصلاً فرستاده نمیشود. نتیجهای که دقیقاً پنج میلیون سطر باشد از یک نتیجهی بریدهشده قابل تشخیص نیست و بریدهشده گزارش میشود: فایل کاملی که اشتباهاً ناقص برچسب بخورد یک کوئری هزینه دارد، و فایل بریدهای که کامل برچسب بخورد عددی است در یک گزارش که بیصدا غلط است.- کاری که در وضعیت
runningبماند و ادعایش بیشتر از ۴۵ دقیقه قدیمی باشد، دوباره برداشته میشود، پس ورکری که کشته شده کار را برای همیشه گیر نمیاندازد. ساختن یک فایل حداکثر ۳۰ دقیقه وقت دارد. expiredیعنی جاروکش فایل را پاک کرده و سطر را نگه داشته، تا سابقهی اینکه خروجی گرفته شد و چه کسی خواستش، از خود فایل بیشتر عمر کند.
پاسخ اصلاً کلید next_cursor ندارد، پس کلاینتی که آن را بخواند بهجای رشته تهی هیچچیز میگیرد، و has_more هرگز true نمیشود. هیچجای API مقداری به next_cursor نمیدهد، پس رفتار پاکت صفحه روی هر مسیری که آن را برمیگرداند همین است و ویژه خروجیها نیست. پارامتر cursor خوانده و دور ریخته میشود. limit پیشفرض ۲۵ است و روی ۱۰۰ سقف میخورد. یعنی فقط ۱۰۰ کار آخر، از جدید به قدیم، در دسترساند و کارهای قدیمیتر از راه API قابل دیدن نیستند.
GET /v1/exports/{id} برای دیدن یک کار تنها، وجود ندارد. تنها راه، گرفتن فهرست و پیدا کردن شناسه در آن است.
گرفتن فایل
GET /v1/exports/{id}/download روی میزبان مدیریت وجود ندارد. فقط روی گوشدهندهی کنترلی داشبورد ثبت شده، که عمداً از بیرون شبکهی داخلی قابل آدرسدهی نیست.
نتیجهاش را صریح میگوییم: یکپارچهسازی شما میتواند از راه api.segmentic.net خروجی بسازد و هیچ راه برنامهای برای برداشتن بایتهایش ندارد. هیچ نشانی امضاشدهای، هیچ فضای ذخیرهسازی شیئی و هیچ فیلد download_url در کل کد وجود ندارد. فایل را باید یک آدم از پنل بردارد، از «گزارشها» و تب «خروجیها». همان صفحه خروجی هم در صف میگذارد، پس یک خروجی موردی اصلاً به کلید API احتیاج ندارد.
اگر برنامهی شما هفتگی به دادهی خام نیاز دارد، تا وقتی این مسیر باز نشده، خروجی صفشده جواب شما نیست.
ستونهای هر خروجی
هر منبع، فهرست ستونش را صریح مینویسد و از SELECT * استفاده نمیکند، چون خروجی فایلی است که از ساختمان بیرون میرود و SELECT * یعنی روزی که کسی ستونی با شناسهی هششده یا پرچم داخلی اضافه کند، آن ستون بیصدا در دانلود بعدی همهی مشتریها ظاهر میشود.
events، ۲۳ ستون به همین ترتیب: message_id، type، name، user_id، anonymous_id، session_id، event_time، received_at، revenue، currency، props_str، props_num، app_version، device_type، os_name، country، region، city، page_url، page_path، utm_source، utm_medium، utm_campaign.
profiles، ۲۷ ستون: user_id، email، phone، first_name، last_name، gender، city، region، country، language، timezone، device_type، os_name، app_version، traits، traits_num، has_push، has_email، has_phone، push_opt_in، email_opt_in، sms_opt_in، total_events، total_revenue، order_count، first_seen، last_seen.
ip و national_id عمداً در خروجی پروندهها نیستند: خروجی، نسخهای از داده است که کمترین محافظت را دارد، و کد ملی در یک فایل اکسل روی لپتاپ کسی، بدترین سطری است که این پایگاه داده میتواند از دست بدهد.
messages، ۱۸ ستون از Postgres: message_id، user_id، channel، category، transport، campaign_id، journey_id، node_id، variant، topic_id، status، reason، gateway، gateway_id، delivery، delivery_detail، delivered_at، sent_at.
segment، ۲۰ ستون: user_id، email، phone، first_name، last_name، gender، city، region، country، total_events، total_revenue، order_count، first_seen، last_seen، has_push، has_email، has_phone، push_opt_in، email_opt_in، sms_opt_in. نقشهی ویژگیها اینجا نیست چون کلیدهایش برای هر کاربر فرق میکند و بدون اسکن کل مخاطب، به مجموعهی ثابتی از ستون تبدیل نمیشود.
خروجی سگمنت همان کامپایلری را استفاده میکند که پیشنمایش داشبورد استفاده میکند، تا فایل و پیشنمایش نتوانند دربارهی اینکه چه کسی در مخاطب هست اختلاف پیدا کنند.
قالب و رمزگذاری
NDJSON: هر سطر یک شیء JSON کامل، کلیددار با نام ستون، تا اضافهشدن یک ستون در آینده اندیس همهی چیزهای پاییندست را یکی جابهجا نکند. زمانها بهصورت RFC3339Nano در UTC میآیند. نقشهی خالی {} میشود، هرگز null. عددها عدد میمانند.
CSV: با BOM یونیکد شروع میشود، وگرنه اکسل روی ویندوز هر نام فارسی را درهم و ناخوانا نشان میدهد. سلولی که با = یا + یا - یا @ یا تب یا CR شروع شود، یک تب جلویش گذاشته میشود، چون ویژگیهای پرونده از کاربران نهایی خود مشتری میآید و کسی که فایل را باز میکند، کارمند مشتری ما است، روی لپتاپ خودش، داخل شبکهی خودش.
در CSV، بولیها به کلمهی بله و خیر ترجمه میشوند، اعداد اعشاری با نماد ساده نوشته میشوند نه نماد علمی، و زمان صفر سلول خالی میشود نه سال ۱۹۷۰ که مثل تاریخ واقعی به نظر میرسد و کسی ممکن است رویش تصمیم بگیرد.
نام ستونها فقط در یک حالت فارسی است: format برابر csv و kind برابر segment. باقی ترکیبها نام ماشینی میگیرند، چون NDJSON را لودری میخواند که روی نام فیلد کلید میزند و کلید JSON با متن فارسی، برای هر خط لولهی پاییندست دشمن است.
xlsx از صف خروجی درنمیآید. فقط csv و ndjson رمزگذار دارند.
سقف سطر و انقضا
سقف پنج میلیون سطر روی هر چهار نوع اعمال میشود، بهصورت LIMIT ساده در SQL. فراتر از آن، فایل چیزی نیست که کسی بازش کند؛ خط لولهای است که باید مستقیم از انبار داده بخواند.
وقتی به سقف بخورد، کار truncated: true میگیرد و پنل روی همان سطر هشدار میگذارد. مدت درازی بیصدا بود: نه در سطر کار و نه در هیچ پاسخی چیزی نمیگفت که خروجی به سقف خورده، rows_written فقط همان عدد سقف را نشان میداد، و کسی به عددی که مشکوکانه رند است دقت نمیکند. اگر دادهی شما ممکن است به پنج میلیون نزدیک شود، بازه را خودتان تکه کنید و به این پرچم تکیه نکنید که بعدا خبرتان کند.
فایل بعد از ۷ روز (۱۶۸ ساعت) پاک میشود و همین عدد در پاسخ ۲۰۲ بهصورت expires_after_hours منتشر میشود. فایلی که آدرس ایمیل همهی مشتریها را دارد و برای همیشه روی یک اشتراک میماند، همان چیزی است که یک خروجی بیدقت را به نشت تبدیل میکند، و چون کسی یادش نمیماند پاکش کند، پلتفرم پاک میکند.
مقصد فایل روی دیسک است، با دسترسی پوشهی 0700 و فایل 0600. پیادهسازی S3 یا فضای شیئی وجود ندارد؛ نصبهای داخل سازمان یک والیوم متصل دارند و نقطهی پایانی S3 ندارند.
وارد کردن داده
وارد کردن CSV فقط از پنل ممکن است. روی api.segmentic.net هیچ نقطهی پایانی چندبخشی برای آپلود فایل وجود ندارد. POST /v1/imports و GET /v1/imports/{id} وجود ندارند. آنچه هست همگام است، و در ادامه توضیح داده میشود تا بدانید پنل دقیقاً چه میکند.
هر سه نقطهی پایانی ورود، دسترسی profile.write میخواهند، از جمله inspect که چیزی ذخیره نمیکند: آن هم مرحلهای است که فایل اکسل مشتری را میخواند. viewer و analyst و approver و finance خطای ۴۰۳ میگیرند؛ marketer قبول میشود.
کاربران از CSV
POST /v1/import/inspect فایل را میخواند، حدس نگاشت را برمیگرداند و چیزی نمینویسد:
{
"header": ["email", "موبایل", "امتیاز"],
"preview": [["a@b.com", "09123456789", "1500"]],
"total": 1,
"mapping": {"columns": [
{"index": 0, "field": "email", "name": "email"},
{"index": 1, "field": "phone", "name": "موبایل"},
{"index": 2, "field": "trait", "name": "امتیاز"}
]}
}
preview حداکثر ۵ سطر است و total تعداد سطرهای داده بدون سطرهای خالی.
POST /v1/import/users همان فایل را واقعاً وارد میکند. فرم چندبخشی با بخش فایل به نام file، بهعلاوهی دو فیلد اختیاری: mapping که یک شیء JSON است و حدس را کاملاً کنار میگذارد، و dry_run.
dry_run پیشفرض خاموش است. فقط رشتهی دقیق "true" آن را روشن میکند و هر چیز دیگری خاموش است. اگر این فیلد را جا بیندازید، پروندههای واقعی نوشته میشوند و کمپینها بعداً همانها را هدف میگیرند.
{
"total": 2,
"accepted": 1,
"rejected": 1,
"errors": [{"row": 3, "column": "موبایل", "value": "rubbish", "reason": "..."}],
"truncated": false,
"dry_run": false,
"ingested": 1
}
row از یک شمرده میشود و سطر سرستون، سطر شماره یک است، پس اولین سطر داده، سطر شماره ۲ است. truncated میگوید فهرست خطاها سر ۵۰ تا بریده شده، تا «۵۰ مشکل» با «دقیقاً ۵۰ مشکل» اشتباه گرفته نشود.
ingested میتواند از accepted کمتر باشد، اگر باس بعضی را نپذیرفته باشد. وجود دارد چون گفتن «بیست هزار نفر وارد شد» وقتی نوزده هزارتا رسیده، همان دروغی است که یک هفته بعد بهشکل کمپینی که به آدمهای کمتری رسید، رو میشود.
خطاهای سطح فایل، ۴۰۰ با کد invalid_file میدهند: سرستون ندارد، سطر داده ندارد، هیچ ستونی به شناسه یا ایمیل یا موبایل نگاشت نشده، سطر بیشتر از حد، ستون بیشتر از حد، فایل انتخاب نشده، فایل بزرگتر از حد. شکست جزئی، ۵۰۳ با کد partial_import میدهد و کل result را همراهش میفرستد، چون بخشی از فایل واقعاً داخل رفته و گفتن «همهاش شکست خورد» یعنی اپراتور فایل را دوباره آپلود میکند.
| سقف | عدد |
|---|---|
| سطر داده در هر فایل | ۵۰۰ هزار |
| ستون سرستون | ۱۰۰ |
| بایت در یک سلول | ۴۰۹۶ |
| حجم فایل | ۶۴ مگابایت (۶۷۱۰۸۸۶۴ بایت) |
| سطر خراب گزارششده | ۵۰ |
GET /v1/import/fields فهرست فیلدها و دو عدد max_rows و max_bytes را منتشر میکند.
نگاشت ستون
نگاشت {"columns": [{"index": 0, "field": "email", "name": "email"}]} است. ستونها با اندیس آدرس داده میشوند نه با متن سرستون، چون فایلهای اکسل مرتب سرستون تکراری یا خالی دارند و نقشهای که با نام کلید بخورد، یکی از آنها را بیصدا میاندازد.
فیلدهای ممکن: user_id، email، phone، first_name، last_name، gender، birthday، national_id، city، region، country، language، trait، ignore. وقتی field برابر trait باشد، name کلید همان ویژگی است.
حدس خودکار، سرستونهای فارسی و انگلیسی را با هم میشناسد، چون یک تیم بازاریابی ایرانی در یک هفته هم از CRM فارسی خروجی میگیرد و هم از یک ابزار تحلیلی خارجی. «شناسه» و «کد کاربر» و «ایمیل» و «رایانامه» و «موبایل» و «شماره تماس» و «نام خانوادگی» و «کد ملی» و «استان» و همتاهای انگلیسیشان شناخته میشوند. سرستونها از تطبیق فارسی رد میشوند، پس شکل عربی «ی» و «ک» هم مثل شکل فارسیشان میخورد.
سرستونی که به هیچچیز نخورد، ویژگیای با کلید همان متن سرستون میشود. ستون دومی که همان هویت قبلی را ادعا کند هم ویژگی میشود، چون دو ستون email یعنی یکی از آن دو چیز دیگری است. سرستون خالی ignore میشود.
جداکننده حدس زده میشود، نه فرض. کاما و نقطهویرگول و تب هر سه بررسی میشوند و امتیاز بر اساس یکدستی در شش خط اول است، نه شمارش خام، تا کامای داخل متن فارسی داخل گیومه برنده نشود. اکسل روی ویندوز فارسی نقطهویرگول مینویسد و فایلی که بیصدا بهشکل یک ستون غولپیکر پارس شود، رایجترین تیکت پشتیبانی این حوزه است.
BOM ابتدای فایل حذف میشود، وگرنه email با یک نویسهی نامرئی جلویش میرسد و بیصدا ویژگی سفارشی میشود. سطرهای ناهمطول تحمل میشوند و سطرهای خالی رد میشوند.
تبدیل هر سطر
خطای هر سطر جمع میشود و کار متوقف نمیشود: فایل بیستهزارسطری با سه شمارهی موبایل خراب باید نوزده هزار و نهصد و نود و هفت نفر را وارد کند و دربارهی آن سه تا حرف بزند.
| فیلد | قاعده |
|---|---|
| هر فیلد | سلول بلندتر از ۴۰۹۶ بایت، سطر را رد میکند |
| هر فیلد | سلول خالی کلاً رد میشود |
user_id | ارقام فارسی به لاتین تا میشوند |
phone | به شکل E.164 ذخیره میشود؛ نامعتبر، سطر را رد میکند |
email | کوچک میشود و شکلش بررسی میشود؛ نامعتبر، سطر را رد میکند |
national_id | رقم کنترلی بررسی میشود؛ نامعتبر، سطر را رد میکند |
birthday | به YYYY-MM-DD گرگوری تبدیل میشود؛ نامعتبر، سطر را رد میکند |
gender | به male یا female نرمال میشود؛ هرگز شکست نمیخورد |
trait | مقدار عددیشکل عدد ذخیره میشود، باقی متن؛ هرگز شکست نمیخورد |
شمارهی موبایل همینجا نرمال میشود نه پاییندست، چون یک شماره که در دو شکل ذخیره شود، دو پرونده برای یک آدم است. ۰۹۱۲۳۴۵۶۷۸۹ به +989123456789 تبدیل میشود.
جنسیت این کلمهها را میشناسد: male، m، «مرد»، «آقا»، «پسر» و female، f، «زن»، «خانم»، «دختر».
ویژگی عددی: مقداری که با صفر شروع شود متن میماند و مقداری بلندتر از ۱۵ نویسه هم متن میماند، چون کد پستی 01234 که به ۱۲۳۴ تبدیل شود غلط است و کد ملی بزرگتر از توان دقت اعشاری، رقمهای آخرش را از دست میدهد. ارقام فارسی هم عدد حساب میشوند.
تاریخ هر دو تقویم و هر دو مجموعهی رقم را میخواند، با جداکنندهی / یا - یا .. سال کمتر از ۱۷۰۰ جلالی خوانده میشود و به گرگوری تبدیل میشود؛ دو تقویم آنقدر از هم دورند که بازهی مبهمی که آدم واقعاً تایپ کند وجود ندارد. 1370/05/12 میشود 1991-08-03، و 1370/13/45 خطاست، چون مرز ماه و روز جلالی قبل از تبدیل بررسی میشود.
اگر user_id نباشد، ایمیل و بعد موبایل جایش را میگیرند. اگر هیچکدام نباشند سطر رد میشود، چون یک identify بدون چیزی برای شناسایی، پروندهی ناشناسی میسازد که هرگز کسی به آن نمیرسد.
خروجی ورود، پاکت identify معمولی است، نه نوشتن مستقیم روی پرونده. اگر مستقیم مینوشت، دو راه واگرا برای ساختن یک سطر داشتیم، و اولین باری که پروندهی واردشده با پروندهی ساختهشدهی SDK اختلاف پیدا میکرد، شمارهای که در یک مسیر 09123456789 و در مسیر دیگر +989123456789 ذخیره شده، کسی نمیتوانست بگوید کدام مسیر غلط بوده. شناسهی حساب همیشه از کلید میآید، هرگز از بار درخواست.
رویدادهای گذشته
POST /v1/import/events رویداد با زمان گذشته مینویسد. این هم فقط از پنل در دسترس است و عمداً روی کالکتور نیست: کالکتور با کلید نوشتن احراز میشود، و کلید نوشتن طبق طراحی داخل اپ موبایل و باندل سایت میرود، یعنی هر کسی که سورس را ببیند یکی دارد. اعتبارنامهی عمومیای که بتواند رویداد با زمان دلخواه گذشته بنویسد، اعتبارنامهای است که میتواند قیف رقیب را بازنویسی کند.
{"events": [
{"type": "track", "event": "order_completed", "user_id": "u1",
"timestamp": "2025-04-02T10:00:00Z"}
]}
- بدنه حداکثر ۶۴ مگابایت.
- آرایهی خالی، خطای ۴۰۰.
- بیشتر از ۱۰ هزار رویداد در هر درخواست، خطای ۴۰۰ و هیچچیز به باس نمیرسد.
- مهلت ۱۰ دقیقه.
dry_runروی این نقطهی پایانی وجود ندارد. فیلدش در پاسخ هست ولی هیچچیز آن را ست نمیکند.
پنجرهی مجاز از سیاست نگهداری همان حساب میآید. اگر روزهای نگهداری رویداد صفر باشد، یعنی نگهداری همیشگی، پنجره ۳۶۵۰ روز است. اگر خواندن سیاست شکست بخورد، پنجره به ۳۰ روز برمیگردد. برای معنی این عدد، دادههای شخصی را ببینید.
تفاوت اصلی با ورود زنده همینجاست: ورود زنده زمان بیرون از پنجره را بیصدا روی لبهی پنجره میگذارد و ۲۰۰ میدهد، که یک سال سفارش را به یک روز غولپیکر تبدیل میکند. این نقطهی پایانی سطر را رد میکند و شمارهی سطرش را میگوید.
{
"total": 10000, "accepted": 9997, "rejected": 3,
"errors": [{"row": 412, "reason": "..."}],
"truncated": false, "dry_run": false,
"oldest": "2024-03-01T08:00:00Z",
"newest": "2026-05-01T21:30:00Z"
}
oldest و newest بازهای است که واقعاً نوشته شد، تا اپراتور قبل از اجرای دستهی بعدی نیممیلیونی مطمئن شود ورود جایی نشسته که منظورش بود. فهرست خطا اینجا سر ۱۰۰ تا بریده میشود، نه ۵۰. شکست جزئی، ۵۰۳ با کد partial_backfill.
برای ورود همزمان از سرور خودتان، POST /v1/events روی همان میزبان مدیریت هست و تا ۵۰۰ رویداد در هر فراخوان میگیرد. تاریخچه را از آن مسیر نبرید. قواعد این صفحه آنجا اعمال نمیشوند: پنجرهی ثابت ۳۰ روز است، سیاست نگهداری حساب خوانده نمیشود، و هر زمانی قدیمیتر از ۳۰ روز بیصدا روی لبهی همان پنجره نوشته میشود و پاسخ ۲۰۰ است. یعنی یک سال سفارش، یک روز غولپیکر میشود و هیچچیز در پاسخ نمیگوید. ارسال از سرور را ببینید.
آنچه فقط در پنل هست
اینها ساخته شدهاند، کار میکنند و تست دارند، ولی هیچ آدرسی روی api.segmentic.net ندارند:
- تحلیل مسیر.
POST /v1/reports/pathsوجود ندارد. - امتیاز ریزش، درگیری و RFM، هم خلاصه و هم فهرست اعضا.
- کاوش رویداد و نمای کلی حساب.
- سری زمانی سناریو و کمپین.
- گزارشهای زمانبندیشده و دیباگر رویداد.
- فهرست قیفهای ذخیرهشده.
GET/POST /v1/funnelsو بقیهی CRUD آن فقط روی کنترلپلیناند؛ فهرست قیفهای ذخیرهشده را ببینید. - موتور داشبوردساز، یعنی
/v1/widgets/queryوfunnelوcohortوvalidate. رندر یک داشبورد ذخیرهشده هم وجود ندارد. - گزارش پیامهای پنل با جستوجوی بازه زمانی، کمپین، کاربر، گیرنده، شناسه پیام، کانال، نتیجه و آزمایشی بودن. خروجی کامل فیلترشده از
GET /v1/messages.csvوGET /v1/messages.jsonبه شکل جریانی دریافت میشود و به صفحهٔ فعلی محدود نیست. - خروجی دفتر مالی و خروجی گزارش ممیزی.
GET /v1/segments/{id}/exportکه تنها جایی است که xlsx تولید میشود. این مسیر جریانی است و سقفش یک میلیون سطر، باز هم بیصدا. عیب شناختهشدهای هم دارد که در خود کد نوشته شده: کد ۲۰۰ و هدرها قبل از خواندن اولین سطر فرستاده میشوند، پس شکست وسط جریان نمیتواند به ۵۰۳ تبدیل شود و بهجایش اتصال قطع میشود. هیچ شمارش سطری و هیچ پرچم کاملبودنی روی سیم نیست.- وارد کردن CSV و بکفیل رویداد، که بالاتر شرحشان آمد.
آنچه ممکن نیست
فهرست صادقانهی کارهایی که با کلید API نمیشود کرد:
- برداشتن بایتهای یک خروجی صفشده.
- خواندن وضعیت یک کار خروجی با شناسهاش.
- رفتن جلوتر از ۱۰۰ کار خروجی آخر.
- گرفتن xlsx از صف خروجی.
- انتخاب ستون یا سقف سطر برای خروجی.
- خواندن سقفهای تحلیلی بهصورت برنامهای.
GET /v1/reports/limitsوجود ندارد وGET /v1/capabilitiesهیچکدامشان را منتشر نمیکند. - گرفتن جمعبندی تحویل و درگیری پیام.
GET /v1/reports/messagesوجود ندارد. - کشف مقدارهای یک ویژگی. هیچ نقطهی پایانی مقدارهای متمایز یک ویژگی را فهرست نمیکند؛
GET /v1/schema/eventsفقط کلیدها را میدهد. - فهمیدن اینکه نام رویدادی که نوشتهاید غلط است.
- انتخاب زبان پاسخ.
Accept-Languageروی این میزبان اصلاً پارس نمیشود و همهی متنهای محلیشده فارسیاند. - گرفتن کد خطای متمایز برای گزارش نامعتبر یا خرابی انبار داده.
- دریافت فراخوان برگشتی در پایان یک خروجی یا یک ورود. وبهوکی برای این دو وجود ندارد.
برای شکل خطاها و کدها، خطاها را ببینید. برای سقفها، محدودیتها.