نسخهبندی API و تغییرها
چه چیزی بدون خبر عوض میشود، چه چیزی نمیشود، و اینکه هر تغییر API چطور در همان تغییر مستند میشود.
نسخه یک بخش از مسیر است
نسخه امروز v1 است و در مسیر نشانی مینشیند. روی هر دو میزبان همینطور است: https://in.segmentic.net/v1/batch و https://api.segmentic.net/v1/whoami.
جای دیگری نسخه نگهداری نمیشود. هدر نسخه خوانده نمیشود، Accept سفارشی خوانده نمیشود، پارامتر پرسوجوی نسخه خوانده نمیشود، و بازبینی تاریخمحور وجود ندارد. اگر نسخه را جایی جز مسیر بفرستید، بیصدا نادیده گرفته میشود.
دلیلش ساده است: یک بخش از مسیر در لاگ، در گزارش خطا و در تنظیمات پراکسی دیده میشود. هدر دیده نمیشود و در مسیر عبور گم میشود. وقتی نیمهشب دارید یک درخواست شکستخورده را دنبال میکنید، مسیر تنها چیزی است که همهجا هست.
هیچ بخش /api در مسیر نیست. مسیر درست /v1/events روی میزبان api.segmentic.net است، نه /api/v1/events. آدرسی که آن بخش اضافه را داشته باشد 404 با کد unknown_endpoint میگیرد و متن پاسخ، متد و مسیری را که فرستادید میگوید.
تا امروز فقط یک نسخه منتشر شده است. v2 وجود ندارد و تاریخی هم برایش اعلام نشده است.
قاعده خانگی برای عوضکردن نسخه یک جمله است: نسخه فقط وقتی عوض میشود که شکل پاسخ طوری تغییر کند که یک یکپارچهسازی موجود متوجه آن بشود. یعنی معیار، اندازه تغییر سمت ما نیست. معیار این است که کدی که دیروز کار میکرد امروز هم کار میکند یا نه. بازنویسی کامل یک بخش درونی، اگر پاسخ همان شکل قبلی را داشته باشد، نسخه را عوض نمیکند. حذف یک کلید کوچک، عوض میکند.
تغییر شکننده چیست
اینها شکنندهاند و بدون عوضشدن نسخه انجام نمیشوند:
- حذف یک کلید از پاسخ
- عوضشدن نوع یک کلید، مثلا از رشته به عدد یا از مقدار ساده به شیء
- حذف یک نقطه پایانی یا تغییر مسیر آن
- اجباریشدن فیلدی که تا دیروز اختیاری بود
- تنگترشدن مقدارهای پذیرفتهشده یک ورودی
- عوضشدن معنای یک کلید، وقتی نام و نوعش سر جایش میماند
- عوضشدن مقدار پیشفرض یک پارامتر، طوری که خروجی فرق کند
- عوضشدن کد وضعیت پاسخ برای حالتی که از قبل وجود داشت
خطرناکترین مورد این فهرست ششمی است. حذف یک کلید پرصدا از کار میافتد و شما همان روز میفهمید. عوضشدن معنای یک کلید بیصدا اشتباه میشود و ممکن است هفتهها بعد در گزارش شما پیدا شود. به همین دلیل آن را همسنگ حذف حساب میکنیم.
وقتی میخواهیم پاسخ کاملتری بدهیم، پاسخ کاملتر زیر یک کلید تازه کنار کلید قدیمی مینشیند و شکل کلید قدیمی دست نمیخورد. فرض کنید count یک عدد است و بعدا لازم میشود تفکیک آن عدد هم برگردانده شود. کاری که نمیکنیم این است که count را به یک شیء تبدیل کنیم. کاری که میکنیم این است که count_breakdown را کنارش اضافه کنیم. count همان عدد میماند، با همان معنا.
بیشتر فشارهایی که بهنظر میرسد «نسخه تازه لازم است»، در واقع «باید بیشتر برگردانیم» است، و بیشتر، کنار قدیمی جا میشود. عمر طولانی v1 نتیجه همین یک قاعده است.
هزینه این قاعده را هم پنهان نمیکنیم. پاسخها با گذشت زمان بزرگتر و شلوغتر میشوند و چند کلید در آنها یادگاری گذشتهاند. این را در برابر شکستن اتصال مشتری پذیرفتهایم. نمونهاش همین امروز در محصول هست: GET /v1/schema/traits هنوز traits را بهصورت آرایه ساده نامها برمیگرداند، چون چیزی بیرون آن را بهعنوان رشته پیمایش میکند، و جواب کاملتر در کلید دوم schema کنارش نشسته است.
تغییر شکننده چه چیزی نیست
اینها هر زمان، بدون اعلام قبلی و بدون عوضشدن نسخه انجام میشوند:
- افزودن یک کلید تازه به پاسخ
- افزودن یک فیلد اختیاری تازه به ورودی
- افزودن یک نقطه پایانی تازه
- افزودن یک مقدار تازه به فهرست مقدارهای مجاز یک فیلد
- افزودن یک هدر تازه به پاسخ
- عوضشدن ترتیب کلیدها در شیء JSON
- رفع اشکالی که پاسخ را با مستندات هماهنگ میکند
- تغییر سرعت پاسخ و جزئیات درونی پیادهسازی
دو مورد از این فهرست معمولا یکپارچهسازیها را غافلگیر میکند: مقدار تازه در یک فیلد شمارشی، و کلید تازه در پاسخ. هر دو غیرشکننده حساب میشوند، پس کد شما باید هر دو را تحمل کند.
مثال زنده از همین محصول: وضعیت یک خروجی امروز یکی از queued، running، ready، failed یا expired است. اگر روزی وضعیت ششمی اضافه شود، این یک تغییر افزایشی است و اعلام قبلی نمیگیرد. کدی که روی این پنج مقدار switch میزند و شاخه پیشفرض ندارد، همان روز میافتد.
کلاینتی که تغییر معمولی ما را تحمل کند
اگر از این صفحه فقط یک چیز را پیاده کنید، همین باشد: کلید ناشناس را نادیده بگیرید و مقدار ناشناس را به شاخه پیشفرض بفرستید. بقیه بندها میگویند ما چه میکنیم؛ این بند تنها چیزی است که سمت شماست.
بهطور مشخص:
- در Go،
DisallowUnknownFieldsرا روی پاسخهای ما روشن نکنید - در Java و Jackson،
FAIL_ON_UNKNOWN_PROPERTIESرا خاموش بگذارید - در هر زبان دیگری، اعتبارسنجی سختگیر شمای پاسخ را روی مسیر اصلی نگذارید
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
)
// Only the three fields this program uses. A key we add next month is
// decoded into nothing and the program carries on. A strict decoder would
// return an error instead, and the caller would read that as "the API is
// down" on the day we shipped a harmless addition.
type Whoami struct {
TenantID uint32 `json:"tenant_id"`
Role string `json:"role"`
Permissions []string `json:"permissions"`
}
func whoami(ctx context.Context, key string) (Whoami, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
"https://api.segmentic.net/v1/whoami", nil)
if err != nil {
return Whoami{}, err
}
req.Header.Set("Authorization", "Bearer "+key)
res, err := http.DefaultClient.Do(req)
if err != nil {
return Whoami{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return Whoami{}, fmt.Errorf("whoami: http %d", res.StatusCode)
}
var out Whoami
// No DisallowUnknownFields here, deliberately.
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
return Whoami{}, err
}
return out, nil
}
func main() {
me, err := whoami(context.Background(), os.Getenv("SEGMENTIC_API_KEY"))
if err != nil {
fmt.Println("could not read whoami:", err)
os.Exit(1)
}
fmt.Println(me.Role, me.Permissions)
}
سه عادت دیگر هم اتصال را شکننده میکند و از سمت ما قابل جبران نیست:
- تکیه بر ترتیب کلیدها در JSON
- تکیه بر نبودن یک کلید، بهجای بررسی مقدار آن
- خواندن تاریخ با بریدن رشته، بهجای تجزیه کامل آن
یک تبصره که سردرگمی میسازد اگر گفته نشود: POST /v1/messages در ورودی فیلد ناشناس را رد میکند. این خلاف قاعده بالا نیست. قاعده بالا درباره خواندن پاسخ ماست. آن مسیر روی ورودی سختگیر است چون کسی که idempotency_key را غلط تایپ کرده، وگرنه در هر تلاش دوباره یک کلید تازه میگرفت و بهازای هر تلاش یک پیام میفرستاد. هیچ مسیر دیگری روی هیچکدام از دو میزبان، فیلد ناشناس ورودی را رد نمیکند.
توانمندیها، جواب زمان اجرا
پرسش «آیا این نصب میتواند فلان کار را بکند» یک جواب زمان اجرا دارد، نه یک جواب مستند:
curl -s https://api.segmentic.net/v1/capabilities \
-H "Authorization: Bearer sk_seg_..."
{
"version": "v1",
"features": {
"segments": true,
"campaigns": true,
"analytics": true,
"transactional": true,
"export": false,
"import": true,
"journeys": true,
"ingest": true,
"async_exports": true,
"campaign_approval": true
},
"limits": {
"max_page_size": 100,
"max_preview_rows": 100,
"max_batch_size": 500,
"estimate_sample": 100,
"query_timeout_sec": 30
}
}
این تنها ساز و کار سازگاری رو به جلوی ماست. بهجای اینکه فهرست قابلیتها را در کد خودتان ثابت بنویسید، آن را از همینجا بپرسید. قابلیتی که بعدا اضافه شود، بدون عوضشدن نسخه در همین پاسخ ظاهر میشود و یکپارچهسازی شما بدون تغییر کد آن را میبیند.
دو نصب سگمنتیک واقعا با هم فرق دارند. بیشتر قابلیتها فقط وقتی مسیرشان ثبت میشود که پیکربندیشان وجود داشته باشد، پس کلاینتی که کل سطح را فرض کند دارد علیه یک داستان کد مینویسد. features.transactional که false باشد یعنی POST /v1/messages روی آن نصب 404 میدهد، نه 403.
سقفها هم منتشر میشوند بهجای اینکه فقط مستند شوند، تا هیچ کلاینت و هیچ عاملی عددی را hardcode نکند که ما بعدا عوضش میکنیم. اگر max_batch_size روزی از ۵۰۰ بالاتر برود، کلاینتی که آن را از اینجا خوانده خودش بزرگتر batch میفرستد.
دو نکته دربارهاش:
- این پاسخ را در زمان راهاندازی بخوانید و مدتی نگه دارید. خواندنش پیش از هر فراخوان، بار بیجهت است و یک واحد از بودجه شما را هم میخورد.
- این نقطه پایانی توانمندی را اعلام میکند، نه شکل داده را. مذاکره نسخه نیست و شکل پاسخ نقاط پایانی دیگر را عوض نمیکند.
و یک صداقت لازم: دو کلید در features هیچ مسیری را روی میزبان مدیریتی روشن یا خاموش نمیکنند. export و journeys فقط خبر میدهند که آن زیرسامانه پیکربندی شده است؛ دو مسیر خروجی را async_exports کنترل میکند نه export، و هیچ مسیر journey روی این میزبان ثبت نشده که journeys بتواند کنترلش کند. ingest و import هم دقیقا یک بولیناند با دو اسم، همانی که POST /v1/events را ثبت میکند. مقدارهای نمونه بالا مال یک استقرارند نه قول ما: هر پرچم یعنی «این زیرسامانه اینجا پیکربندی شده»، پس روی نصب خودتان بخوانیدش. جدول کامل اینکه هر کلید کدام مسیر را کنترل میکند در مرجع API مدیریتی است.
چطور از یک تغییر باخبر میشوید
هر تغییر شکننده دست کم شش ماه پیش از اجراییشدن اعلام میشود.
اعلام از دو راه انجام میشود: صفحه تغییرات در مستندات، و رایانامه به رابط فنی هر حسابی که آن نقطه پایانی را در همان دوره فراخوانده باشد. یعنی اگر از بخشی استفاده نمیکنید، بابت آن رایانامه نمیگیرید.
رفع یک آسیبپذیری امنیتی میتواند این مهلت را کوتاه کند. در آن صورت علت و دامنه تغییر در همان اعلام نوشته میشود.
امروز هیچکدام از این دو راه خودکار نیست، و این را صریح مینویسیم چون نیمهکارهبودنش را نگفتن، بدتر از خود نیمهکارهبودن است. صفحه تغییرات هنوز در این مستندات وجود ندارد. و هیچ فیلدی روی یک حساب، «رابط فنی» را نام نمیبرد، پس گیرنده آن رایانامه را چیزی در محصول انتخاب نمیکند. تا وقتی هر دو ساخته شوند، مطمئنترین راه برای دیدن اینکه چیزی عوض شده، GET /v1/capabilities است و همین مستندات، که با خود تغییر جلو میآید.
تغییر خود سند سیاست، دست کم ۶۰ روز پیش از اجراییشدن اعلام میشود و نسخههای پیشینش بایگانی میشوند، تا بشود دید در روزی که یک یکپارچهسازی نوشته شده، تعهد ما چه بوده است. متن کامل تعهد در سیاست نسخهبندی رابط برنامهنویسی است. اگر جایی از این صفحه با آن سند تعارض داشت، آن سند تعهد است و این صفحه توصیف.
هدر غروب امروز فرستاده نمیشود
امروز هیچ هدر ماشینخوانی برای منسوخسازی فرستاده نمیشود. نه Sunset، نه Deprecation، نه Warning روی پاسخ یک نقطه پایانی در حال منسوخشدن.
جایش همان دو چیزی است که بالاتر گفته شد: صفحه تغییرات و رایانامه. غیر از این دو، سیگنال دیگری وجود ندارد.
این را روشن مینویسیم چون خلافش گران تمام میشود. اگر کسی با این تصور کد بنویسد که سامانه پیش از قطعشدن یک هشدار ماشینخوان میفرستد، آن هشدار هرگز نمیآید و پایش او هیچوقت زنگ نمیزند. آن روز، اولین نشانه یک 404 است روی مسیری که سالها کار میکرد.
اگر روزی این هدرها اضافه شوند، خودش یک تغییر افزایشی است، بدون عوضشدن نسخه انجام میشود و همین صفحه بهروز میشود.
عمر نسخه قدیمی
بعد از انتشار یک نسخه تازه، نسخه قبلی دست کم یک سال زنده میماند و به فراخوانها پاسخ میدهد.
در این بازه، نسخه قدیمی فقط رفع اشکال و اصلاح امنیتی میگیرد و قابلیت تازه روی آن اضافه نمیشود. قابلیت تازه فقط روی نسخه جاری میآید و همین، خودش دلیل مهاجرت است.
بعد از پایان این بازه، فراخوان به مسیر نسخه بازنشسته با خطای 410 رد میشود. عمدا به نسخه تازه هدایت نمیشود: هدایت خاموش یک فراخوان قدیمی به پاسخی با شکل تازه، بدتر از خطاست. خطا را همان لحظه میبینید؛ داده بدشکل را ممکن است هرگز نبینید.
چون تا امروز فقط v1 منتشر شده، هیچ نسخهای بازنشسته نشده و هیچ مسیری روی هیچکدام از دو میزبان امروز 410 نمیدهد. اگر 410 گرفتید، از سگمنتیک نیست؛ از یک پراکسی بین شما و ماست.
مستندات با خود تغییر جلو میآید
بخشی از این محصول، خود محصول است و بخش دیگرش توصیف محصول: همین مرجع API، راهنمای داخل پنل، متن لندینگ، سیاست حریم خصوصی، متن رایانامهها. توصیفها همیشه پاییندست فیچرند و هیچوقت داخل کارت خود فیچر دیده نمیشوند.
نتیجهاش این است: یک فیلد به API اضافه میشود، تستها سبزند، کار تمام بهنظر میرسد، و در همان لحظه مرجع API آن فیلد را ندارد. هیچ تستی قرمز نمیشود و هیچکس بد کار نکرده.
قانون چهارم مخزن سگمنتیک برای همین است: توصیف محصول در همان تغییری منتشر میشود که خود محصول. این یک ترجیح سلیقهای نیست، شرط پذیرش کار است، همسنگ سه قانون دیگر که درباره امنیت و دوزبانهبودناند. متنش میگوید توصیفی که با محصول نخواند، از نبودنش بدتر است، چون کسی به آن اعتماد میکند و اشتباه میافتد.
برای API، سطر مربوطه در جدول آن قانون این است: نقطه پایانی یا فیلد تازه در API، یعنی مرجع API و نمونه کدش در همان تغییر عوض میشوند.
چه چیزی خودکار بررسی میشود
سه چیز خودکارند. یکی پیش از بیلد اجرا میشود و دو تای دیگر در CI:
- نگهبان مستندات، که پیش از هر بیلد سایت اجرا میشود و بیلد را با کد خروج ۱ میخواباند. از این سه، تنها چیزی است که به یک بیلد چسبیده.
npm run check:localesدر پوشه پنل، که هر رشته نمایشی را در برابر جفت انگلیسیاش میگذارد. این سطر قانون دوم است. قدمی درCIاست و دستوری که خودت هم میتوانی بزنی؛ هیچ بیلدی در پنل صدایش نمیزند.scripts/check-api-is-documented.mjs، که قدم جداگانهای درCIاست و قانون هفتم را میسنجد. دو جهت دارد که پایینتر آمدهاند.
جهت اول: مسیری که روی کالکتور یا API مدیریتی ثبت شده باشد و در مرجع همان سطح نباشد، بیلد را قرمز میکند. هشت مسیر مستثنا هستند و اسکریپت هر کدام را با دلیلش مینویسد: پیکسل باز شدن، دو مسیر لغو اشتراک، دو مسیر مرکز ترجیحات، ریدایرکت لینک کوتاه، ورودی برگشتیها، و preflight مربوط به CORS. هیچکدام مسیری نیست که مشتری صدایش بزند.
جهت دوم باریکتر از چیزی است که به گوش میآید. در هر صفحهای، نمونه کدی که خط درخواستش in.segmentic.net یا api.segmentic.net را نام ببرد با مسیری که آن میزبان سرو نمیکند، بیلد را قرمز میکند. خط درخواستی که بدون میزبان نوشته شده بررسی نمیشود، و این عمدی است: همان شکلی است که صفحههای صادق برای نشاندادن فراخوانی به کار میبرند که مشتری نمیتواند بزند. صفحهٔ حریم خصوصی عبارت POST /v1/privacy/erasures را زیر جملهای مینویسد که میگوید با curl نمیشود، و پیام تراکنشی مسیرهای قالب را زیر هشداری میآورد که میگوید میزبان مدیریتی هیچکدامشان را سرو نمیکند. بررسیای که اینها را بخواباند، به همه یاد میدهد بهجای درستکردن چیزی، توضیح را پاک کنند.
نگهبان مستندات اینها را میگیرد:
- صفحهای که فقط به یک زبان نوشته شده. صفحهای که به فارسی هست و به انگلیسی نیست، کار ناتمام است نه کار تمام که منتظر ترجمه است.
- دو زبانی که لنگرهای یکسان ندارند. تعویض زبان وسط صفحه باید جای شما را حفظ کند، و این فقط وقتی میماند که چیزی بررسی کند.
- em dash و دو خویشاوندش، که قانون اول است.
- نشانههای نوشتار ماشینی در فارسی: اعراب، فلش راست در متن راستبهچپ، نقطه وسط، ارقام عربی، ی و ک عربی.
- لینکی که به صفحه یا لنگری اشاره کند که وجود ندارد.
- ارقام لاتین در نثر فارسی، بهصورت هشدار.
چه چیزی خودکار بررسی نمیشود
اینجا اسکریپتی نیست که همهاش را بگیرد و وانمود نمیکنیم هست.
هیچ چیزی نام یک فیلد مستندشده را با کد مقایسه نمیکند. اگر فردا فیلدی از یک پاسخ حذف شود و این صفحه هنوز نامش را ببرد، نگهبان سبز میماند و بیلد رد میشود. آنچه این را میگیرد یک آدم است که پیش از باز کردن Pull Request جدول قانون چهارم را از بالا تا پایین میخواند و میپرسد کدام سطرش به این تغییر میخورد.
پس اگر جایی از این مستندات با رفتار واقعی API مخالف بود، API درست است. آن مورد را به پشتیبانی بگویید؛ یک صفحه غلط، دقیقا همان چیزی است که این قانون برای نداشتنش نوشته شده.
امروز چه چیزی درست است
خلاصه صادقانه وضع امروز، برای کسی که دارد یک اتصال تولیدی مینویسد:
| چیز | امروز |
|---|---|
| نسخه منتشرشده | فقط v1 |
v2 | وجود ندارد، تاریخی هم اعلام نشده |
| جای نسخه | بخشی از مسیر، روی هر دو میزبان |
| صفحه تغییرات | هنوز ساخته نشده |
| رایانامه اعلام تغییر | هیچ فیلدی روی حساب رابط فنی را نام نمیبرد |
هدر Sunset و Deprecation | فرستاده نمیشود |
مسیری که 410 بدهد | وجود ندارد، چون نسخهای بازنشسته نشده |
| مذاکره نسخه با هدر | خوانده نمیشود |
سه چیز که امروز میتوانید به آنها تکیه کنید:
GET /v1/capabilitiesدر زمان اجرا، برای اینکه بدانید این نصب چه دارد و سقفهایش چقدر است.- همین مستندات، که طبق قانون چهارم با خود تغییر منتشر میشود. توصیف ماشینخوان همین سطح در فایل OpenAPI است.
- صفحه سیاست نسخهبندی رابط برنامهنویسی، که تعهد نوشتهشده ماست و نسخههای پیشینش بایگانی میشود.
و یک چیز که امروز به آن تکیه نکنید: هیچ سیگنال ماشینخوانی پیش از یک تغییر شکننده نمیآید. اگر پایش میسازید، روی کد خطا و روی GET /v1/status بسازید، نه روی هدری که نمیآید.