SDK اندروید
SDK رسمی اندروید برای ثبت رویداد، هویت، پوش و پیام درونبرنامهای.
SDK اندروید رویداد ثبت میکند، هویت را تشخیص میدهد، پوش میگیرد و پیام درونبرنامهای را میکشد. سه ماژول Gradle است، بهعلاوه یک اپ نمونه که دقیقا همانطور صدایشان میزند که اپ شما میزند.
چطور به بیلد شما میرسد
این SDK به صورت سورس و از روی مخزن میآید، نه از یک مخزن بسته. یک بار اضافهاش میکنید و بعد همان مختصات همیشگی را مینویسید:
implementation("net.segmentic:segmentic-android:0.1.0")
دو مسیر شما را به اینجا میرساند و هر دو در افزودن به اپ با تمام دستورها نوشته شدهاند: بیلد ترکیبی با includeBuild، یا انتشار محلی با publishToMavenLocal. اگر مخزن SDK کنار پروژه خودتان است اولی را بردارید، وگرنه دومی را.
هرچه پایینتر میآید درباره کدی است که اجرا شده، روی شبیهساز اندروید ۱۵ و نه روی کاغذ: یک توکن واقعی فایربیس که با POST /v1/devices ثبت شد، پیامی که FCM v1 هم در حالت باز بودن اپ و هم در پسزمینه تحویل داد، و یک بنر درونبرنامهای که کشیده شد و بار دوم سقف تکرار خودش جلویش را گرفت.
سه ماژول
| ماژول | چیست | کجا تست میشود |
|---|---|---|
segmentic-core | کاتلین خالص، بدون حتی یک import اندرویدی. صف، backoff، شکل سیم، قاعدههای پیام درونبرنامهای، حالت سهگانه مجوزها | JVM ساده، در چند میلیثانیه، بدون شبیهساز |
segmentic-android | لایه نازک اندروید: فایل کجا بنشیند، دستگاه چه میگوید، کار روی کدام ترد برود، و رندر پیام درونبرنامهای | دستگاه یا شبیهساز |
sample | اپی که SDK را دقیقا مثل یک مشتری صدا میزند. منتشر نمیشود | شبیهساز |
دلیل این تقسیم در خود settings.gradle.kts نوشته شده: هر قاعدهای که میشود اشتباه نوشت در ماژول اول است و روی یک JVM ساده تست میشود. قاعدهای که فقط با شبیهساز قابل بررسی باشد، قاعدهای است که کمتر بررسی میشود.
شما فقط segmentic-android را اعلام میکنید. segmentic-core با آن میآید، چون POM ماژول اندروید وابستگیاش را اعلام کرده است.
افزودن به اپ
مسیر یک: بیلد ترکیبی
اگر مخزن SDK کنار مخزن خودتان است، این سادهترین راه است و هیچ انتشاری نمیخواهد. Gradle خودش مختصات را با پروژه محلی جایگزین میکند.
includeBuild("../segmentic/sdk/android")
dependencies {
implementation("net.segmentic:segmentic-android:0.1.0")
}
مسیر دو: انتشار محلی
یک بار در مخزن SDK اجرا کنید:
cd segmentic/sdk/android
./gradlew :segmentic-core:publishToMavenLocal :segmentic-android:publishToMavenLocal
چهار فایل در ~/.m2/repository/net/segmentic/ مینشیند. بعد در اپ خودتان mavenLocal() را اضافه کنید:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
mavenLocal()
}
}
dependencies {
implementation("net.segmentic:segmentic-android:0.1.0")
}
هر دو ماژول یک sources.jar هم منتشر میکنند. این عمدی است: کامنتهای داخل segmentic-core توضیح میدهند چرا صف از جلو افت میکند و چرا یک 4xx دور ریخته میشود، و کسی که نصفهشب دنبال مشکل اپ خودش میگردد باید بتواند همان را در IDE بخواند نه اینکه حدس بزند.
وقتی روزی مخزن راه بیفتد، دستور انتشارش این است و هیچ چیز دیگری در کد عوض نمیشود:
./gradlew publish \
-PsegmenticRepoUrl=https://example.invalid/maven \
-PsegmenticRepoUser=... \
-PsegmenticRepoPassword=...
بدون وابستگی، و چه چیزی میخرد
segmentic-core هیچ وابستگی تولیدی ندارد. صفر. نه کتابخانه JSON، نه کلاینت HTTP. segmentic-android دقیقا یکی دارد و آن هم segmentic-core است.
یک نکته که README خود SDK نمیگوید و اینجا باید بگوییم: هر دو POM منتشرشده org.jetbrains.kotlin:kotlin-stdlib:2.0.21 را در scope برابر compile اعلام میکنند. پس «بدون وابستگی شخصثالث» درست است، ولی «بدون هیچ وابستگی» تحتاللفظی درست نیست. کتابخانه استاندارد کاتلین را هر اپ کاتلینی از قبل دارد.
چیزی که این میخرد یک چیز مشخص است: SDK نمیتواند نسخه کتابخانهای را بهجای شما انتخاب کند. کتابخانهای که یک پارسر JSON یا یک کلاینت HTTP با خودش میآورد، با نسخهای که اپ شما از قبل دارد تصادم میکند، و بعد شما هستید که باید درخت وابستگی ما را دیباگ کنید.
هیچ منبعی هم داخل کتابخانه نیست: buildConfig = false و androidResources = false. به همین دلیل دکمه بستن پیام درونبرنامهای کاراکتر × است نه یک آیکون؛ یک آیکون اولین منبعی میشد که وارد APK شما میشد.
consumer-rules.pro عمدا خالی از قاعده است. هیچ چیزی با reflection صدا زده نمیشود و هیچ کلاسی با اسم بارگذاری نمیشود، پس R8 آزاد است همهاش را کوچک و نامعوض کند. فایل وجود دارد تا این یک تصمیم ثبتشده بماند، نه چیزی که بعدا کسی باید دوباره از اول نتیجه بگیرد.
اندازههای اندازهگیریشده، از خروجی واقعی publishToMavenLocal:
| فایل | بایت |
|---|---|
segmentic-android-0.1.0.aar | 31730 |
segmentic-android-0.1.0-sources.jar | 6705 |
segmentic-core-0.1.0.jar | 54948 |
segmentic-core-0.1.0-sources.jar | 19996 |
داخل AAR پنج ورودی است: AndroidManifest.xml، classes.jar، R.txt خالی، proguard.txt و یک فایل متادیتا. هیچ منبعی، دقیقا همان چیزی که فایل بیلد قول داده.
عددی که نداریم: هزینه واقعی SDK داخل اپ شما، یعنی تعداد متد یا اضافهشدن حجم dex بعد از R8. هیچ اندازهگیریای از این در مخزن نیست. اندازه AAR و jar را داریم و همان را نوشتیم.
کف پلتفرم: minSdk = 24 (اندروید ۷)، compileSdk = 35، جاوا ۱۷ برای source و target. پایینتر رفتن یعنی وارد کردن قاعدههای desugaring به بیلد شما، برای سهمی از دستگاهها که حالا زیر یک درصد است.
چه چیزی به منیفست شما اضافه میشود
دو چیز، و همین.
<uses-permission android:name="android.permission.INTERNET" />
<queries>
<package android:name="com.google.android.gms" />
</queries>
INTERNET یک مجوز عادی است و از کاربر چیزی نمیپرسد.
ACCESS_NETWORK_STATE عمدا اعلام نشده. با آن میشد گزارش داد که اتصال دیتا است یا وایفای، که ستون قشنگی است و ارزش این را ندارد که بیسروصدا یک مجوز به منیفست کس دیگری اضافه کنیم. کد فقط وقتی آن را میخواند که اپ خودتان از قبل مجوزش را داشته باشد، و در این حالت context.network به رویدادها اضافه میشود. اگر نداشته باشید، آن کلید اصلا فرستاده نمیشود.
بلوک queries لازم است چون اندروید ۱۱ به بعد بستههای دیگر را پنهان میکند. بدون آن، جستوجوی Play Services روی گوشیای که واقعا Play Services دارد NameNotFoundException میدهد، ما has_gms=false گزارش میکنیم، و هر پوش آن دستگاه بیدلیل از مسیر FCM کنار گذاشته میشود.
راهاندازی
یک بار، در Application.onCreate:
package com.example.shop
import android.app.Application
import net.segmentic.sdk.SegmenticOptions
import net.segmentic.sdk.android.Segmentic
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
Segmentic.init(
this,
SegmenticOptions(
writeKey = "wk_seg_...",
apiHost = "https://in.segmentic.net",
),
)
}
}
و در منیفست خودتان:
<application android:name=".MyApp">
</application>
سه نکته که وقت میگیرند اگر ندانید:
حتما Application بدهید، نه Activity. کد context.applicationContext را میگیرد، ولی برای دنبال کردن Activity جلویی به خود Application نیاز دارد. اگر آنچه دادید Application نباشد، یک خط هشدار در logcat میآید و پیام درونبرنامهای دیگر قابل کشیدن نیست. بقیه چیزها کار میکند.
صدا زدن دوباره init نادیده گرفته میشود. متد @Synchronized است و بار دوم فقط «init called twice, ignoring the second call» را لاگ میکند. این با SDK وب فرق دارد، که در آن init دوم کلاینت قبلی را میبندد و جایش را میگیرد.
init یک خواندن کوچک روی همان تردی که صدایش زده انجام میدهد، تا صفی که از اجرای قبلی مانده بارگذاری شود. برای همین جایش Application.onCreate است: چند میلیثانیه آنجا عادی است، و جایگزینش یک getter مسابقهای بود که بدتر است.
تنظیمات
SegmenticOptions یک data class در segmentic-core است. همه پیشفرضها عین SDK وباند، عمدا: مشتریای که هر دو را دارد نباید در یک داشبورد دو رفتار batching متفاوت ببیند.
| گزینه | نوع | پیشفرض | معنی |
|---|---|---|---|
writeKey | String | ندارد، اجباری | کلید نوشتن از پنل. عمومی است و میتواند داخل APK باشد |
apiHost | String | ندارد، اجباری | آدرس Collector، مثلا https://in.segmentic.net |
batchSize | Int | 20 | ارسال بهمحض اینکه این تعداد پیام بافر شد |
flushIntervalMs | Long | 10_000 | حداکثر فاصله بین دو ارسال |
maxQueueSize | Int | 500 | چند پیام حق دارند روی دیسک منتظر بمانند |
maxRetries | Int | 10 | چند شکست پشتسرهم تا آهنگ تلاش مجدد از رشد بایستد |
autoContext | Boolean | true | افزودن اپ و سیستم و صفحه و زبان و منطقه زمانی به هر پیام |
sessionTimeoutMs | Long | 30 * 60_000 | فاصله بیکاری که رویداد بعدی را وارد نشست تازه میکند |
debug | Boolean | false | لاگ در logcat با تگ segmentic |
تنها جایی که این SDK استثنا پرتاب میکند همینجاست. سازنده SegmenticOptions روی writeKey یا apiHost خالی یک IllegalArgumentException میدهد. این عمدی است و در زمان ساخت آبجکت اتفاق میافتد، نه بعدا وسط یک track(). هیچ متد دیگری در هیچ شرایطی throw نمیکند.
مقادیری که قابل احترام گذاشتن نیستند، قبل از هر کاری به بازه برگردانده میشوند:
| فیلد | به این محدود میشود |
|---|---|
apiHost | / انتهایی حذف میشود |
batchSize | بین 1 و 1000 |
flushIntervalMs | دستکم 1000 |
maxQueueSize | دستکم به اندازه batchSize |
maxRetries | بین 1 و 100 |
sessionTimeoutMs | دستکم 1000 |
کف maxQueueSize تزئینی نیست. دستکم یک بسته کامل باید جا شود، وگرنه صفی که پر شده هرگز نمیتواند یک ارسال را سرهم کند و بافر فقط با افت خالی میشود.
سه گزینهای که SDK وب دارد و اینجا نیست: autoPageView (اپ صفحه ندارد)، respectDoNotTrack (اندروید چنین سیگنالی ندارد) و onsite. پیام درونبرنامهای همیشه روشن است و در هر screen() سنجیده میشود.
متدهای عمومی
Segmentic یک object کاتلین است و هر متدش @JvmStatic است، پس از جاوا هم static معمولی دیده میشود.
val isInitialised: Boolean
fun init(context: Context, options: SegmenticOptions)
fun track(event: String, properties: Map<String, Any?>? = null)
fun screen(name: String, properties: Map<String, Any?>? = null)
fun identify(userId: String, traits: Map<String, Any?>? = null)
fun alias(previousId: String)
fun reset()
fun optOut()
fun optIn()
fun isOptedOut(): Boolean
fun registerDevice(tokens: Map<String, String>, hasGms: Boolean? = null)
fun dismissOnsite()
fun flush()
fun stats(): SegmenticStats?
fun anonymousId(): String?
fun userId(): String?
fun shutdown()
نمونه کامل، همانطور که یک اپ فروشگاهی صدایش میزند:
import net.segmentic.sdk.android.Segmentic
// بازدید صفحه، که همان لحظهای است که پیام درونبرنامهای هم سنجیده میشود
Segmentic.screen("cart", mapOf("items" to 3))
Segmentic.track(
"product_viewed",
mapOf("product_id" to "DK-991", "price" to 18_500_000, "currency" to "IRR"),
)
// بعد از ورود: alias خودکار ساخته میشود و تاریخچه ناشناس به این کاربر میچسبد
Segmentic.identify(
"u_123",
mapOf("email" to "ali@example.com", "city" to "شیراز"),
)
Segmentic.track("order_completed", mapOf("revenue" to 2_500_000, "currency" to "IRR"))
// خروج از حساب
Segmentic.reset()
رفتارهایی که ارزش دانستن دارند:
- نام خالی نادیده گرفته میشود، نه throw.
track("")وscreen("")وidentify("")یک خط لاگ مینویسند و برمیگردند. یک فراخوانی تحلیلی هرگز نباید دلیل شکستن صفحه پرداخت مشتری باشد. identifyاولین بار یک پیامaliasجلوتر از خودش صف میکند. فقط وقتیuserIdبا آنچه از قبل ذخیره شده فرق داشته باشد. بدون آن، هر رویداد پیش از اولین ورود مال یک غریبه است و هر قیفی که از مرز ورود عبور کند برای همیشه عدد اشتباه گزارش میدهد. تفصیلش در هویت.resetیکanonymousIdتازه میسازد،userIdرا پاک میکند و نشست را میاندازد. روی گوشی مشترک، خرید نفر بعدی نباید بهحساب کسی برود که تازه رفته. ولی نشانه «قبلا این اپ را باز کرده» را پاک نمیکند: خروج از حساب کسی را کاربر تازه نمیکند، و کمپین «اولین اجرا» نباید بعد از هر خروج برگردد.flush()هیچ چیزی برنمیگرداند. کار را روی ترد شبکه SDK میاندازد و فورا برمیگردد. این با SDK وب فرق دارد که یکPromiseمیدهد. اگر لازم دارید بدانید چه شد،stats()را بخوانید.- هر متدی قبل از
initیکLog.wمینویسد و برمیگردد. هیچ چیز throw نمیشود و هیچ صفی جمع نمیشود. shutdown()برای اپ نیست. هر دو executor را میبندد و کلاینت را null میکند. برای تستهای خود مشتری است: اپی که دارد کشته میشود لازم نیست تمیزکاری کند، و صف از قبل روی دیسک است.
stats() این ۹ فیلد را میدهد و اگر init نشده باشد null است:
| فیلد | نوع | چیست |
|---|---|---|
queued | Int | چند پیام همین حالا روی دیسک منتظرند |
sent | Long | چند پیام Collector پذیرفته است |
dropped | Long | چند پیام افتاده، از پر شدن صف یا از پر بودن دیسک یا از رد دائمی سرور |
consecutiveFailures | Int | چند شکست پشتسرهم |
optedOut | Boolean | انصراف داده شده یا نه |
durableStorage | Boolean | روی اندروید همیشه true است، چون FileStore استفاده میشود |
anonymousId | String | شناسه ناشناس فعلی |
userId | String? | کاربر واردشده، یا null |
devicePending | Boolean | یک ثبت دستگاه هست که هنوز نرسیده و دوباره تلاش میشود |
تردها
این بخش همان چیزی است که مشتری حس میکند.
track،screen،identifyو بقیه فورا برمیگردند. نوشتن روی دیسک روی تردی به نامsegmentic-workانجام میشود، پس هیچ فراخوانی تحلیلی روی ترد اصلی کار I/O نیست.- شبکه ترد دوم است،
segmentic-net. پس یک Collector کند یا خاموش نمیتواندtrack()را پشت یک سوکت منتظر نگه دارد. - هر دو executor تکتردی و daemon و با
Thread.MIN_PRIORITYساخته میشوند. daemon، چون تایمر ما هرگز نباید دلیل زنده ماندن یک پروسه باشد. - تایمر flush یک
scheduleWithFixedDelayبا فاصلهflushIntervalMsاست، وinitبلافاصله یک flush هم میزند تا هرچه در اجرای قبلی نرسیده بود همانجا برود. ثبت دستگاه معلق هم در همان پاس دوباره تلاش میشود. - وقتی صف به
batchSizeمیرسد، ارسال بلافاصله شروع میشود و منتظر تمام شدن بازه نمیماند.
یک جای مشخص هست که استثنا بلعیده میشود، و عمدی است: هر کاری که روی تردهای SDK میرود داخل یک try/catch است که خطا را در سطح error لاگ میکند. این تردها داخل پروسه مشتریاند و یک throwable گرفتهنشده روی ترد پسزمینه کل اپ او را پایین میآورد. انجام دادن این کار بهخاطر یک نوشتن ناموفق تحلیلی قابل دفاع نبود.
صف آفلاین
روی شبکه موبایل ایران دستگاه ساعتها آفلاین میماند. هر پیام قبل از هر تلاش شبکه روی دیسک نوشته میشود، پس کشته شدن اپ، قطع سیگنال و قطعی بکاند هیچ دادهای نمیبرد.
شکل ذخیرهسازی یک خط بهازای هر پیام است:
<message_id>\t<پیام، از قبل به JSON کدشده>\n
دو چیز از این شکل نتیجه میشود و هر دو خود هدفاند:
- هیچ چیز هرگز parse نمیشود. پیام یک بار، هنگام صف شدن، کد شده و همان بایتها به Collector میرسند. مقداری که از کدگذاری جان سالم به در برده، نمیتواند در رفت و برگشت از دیسک خراب شود.
- نوشتنی که نصفه قطع شده، دقیقا به اندازه خط آخر هزینه دارد. هر خط کامل قبل از آن باز هم بارگذاری میشود.
هنگام بارگذاری دو بررسی انجام میشود: خطی که tab ندارد رد میشود، و پیامی که همزمان با { شروع و به } ختم نشود هم رد میشود. بررسی دوم به این خاطر است که نصف یک پیام، اگر فرستاده شود، از سمت Collector بهعنوان بدنه خراب رد میشود و کل بسته پشت سرش را هم با خودش پایین میکشد.
محل ذخیره. یک فایل بهازای هر کلید، در filesDir/segmentic/ اپ شما. نه cache و نه حافظه خارجی: سیستمعامل هر وقت بخواهد cache را پاک میکند، و رویدادی که منتظر تمام شدن یک قطعی است cache نیست؛ از دست دادنش یعنی از دست دادن داده مشتری.
نوشتن مستقیم روی مقصد انجام میشود، بدون فایل موقت و بدون rename. این بیاحتیاطی نیست، تصمیم است: renameTo روی هر درایور ذخیرهسازی اندروید نمیتواند فایل موجود را جایگزین کند، و java.nio.file.Files.move به API 26 نیاز دارد در حالی که کف این SDK ۲۴ است. بهجای اجتناب از قطع شدن نوشتن، قطع شدنش بیخطر شده است.
وقتی صف پر میشود، از جلو افت میکند. بعد از یک قطعی طولانی، تازهترین رویدادها آنهایی هستند که هنوز ارزش داشتن دارند. تعداد افتاده در stats().dropped شمرده میشود و هرگز بیصدا نیست.
سه دلیل افت، که همه در لاگ debug هم میآیند:
| دلیل | کی |
|---|---|
queue_full | از maxQueueSize رد شد |
storage_full | نوشتن روی دیسک شکست خورد، نصف بافر ریخته شد و دوباره تلاش شد |
storage_unavailable | نوشتن دوم هم شکست خورد. کار در حافظه ادامه مییابد و هرچه مانده در خطر است |
حذف تکراری. message_id یک UUID است که یک بار، هنگام صف شدن، ساخته میشود و در هر تلاش مجدد همان میماند. این تمام مبنای امن بودن ارسال دوباره است: بدون آن، ارسال مجدد روی شبکه ضعیف تعداد خرید مشتری را دو برابر میکند.
پاسخهای HTTP، و اینکه SDK با هرکدام چه میکند:
| پاسخ | رفتار |
|---|---|
2xx | پذیرفته شد، از صف پاک میشود، شمارنده شکست صفر میشود |
4xx بجز 429 | برای همیشه دور ریخته میشود، از صف پاک و در dropped شمرده میشود، با یک خط لاگ. بدنهای که سرور رد کرده هرگز پذیرفته نمیشود و نگه داشتنش هر رویداد پشت سرش را قفل میکند |
429 | در صف میماند، دوباره تلاش میشود |
5xx | در صف میماند، دوباره تلاش میشود، backoff اعمال میشود |
کد 0 | یعنی اصلا پاسخ HTTPای نبود: بیسیگنال، خطای DNS، captive portal. جدا از کد واقعی نگه داشته میشود، چون قاطی کردن این دو همانجایی است که یک SDK شروع میکند به تلاش ابدی روی یک 400 |
فهرست کامل کدها و معنایشان در خطاها است.
Backoff جیتر کامل است: پایه یک ثانیه، سقف پنج دقیقه، و توان قبل از اعمال شدن روی ۲۰ محدود میشود تا دستگاهی که ماهها آفلاین بوده نتواند با سرریز به تأخیر منفی برسد. جیتر از خود منحنی مهمتر است: وقتی بکاند برمیگردد، هزاران دستگاهی که همزمان شکست خوردهاند نباید همزمان دوباره تلاش کنند و دوباره بیندازندش.
maxRetries آهنگ را محدود میکند، نه داده را. بعد از رسیدن به سقف، پیامها همانجا در صف میمانند تا اجرای بعدی. کاربر شاید فقط در قطار باشد.
دو flush همزمان نمیشود. flush دوم فورا 0 برمیگرداند بهجای اینکه پشت اولی صف بکشد، چون دو drain همزمان هرکدام همان پیامها را peek میکنند و دو بار میفرستند. این هم با SDK وب فرق دارد که flushها را زنجیر میکند.
مهلتهای شبکه: connect ده ثانیه، read پانزده ثانیه. هر دو تنظیم شدهاند، چون سوکتی بدون مهلت خواندن روی شبکه موبایل میتواند دقیقهها روی یک اتصال نیمهباز آویزان بماند، و این روی تردی است که SDK صاحبش است: آویزان شدنش یعنی صف بدون هیچ خطایی جایی، از تخلیه میایستد. بدنه پاسخ حداکثر تا ۸ کیلوبایت خوانده میشود، چون یک proxy یا captive portal میتواند به POST ما یک مگابایت HTML جواب بدهد.
آنچه روی سیم میرود
POST {apiHost}/v1/batch با هدر Authorization: Bearer wk_seg_... و Content-Type: application/json; charset=utf-8.
sent_at هم روی پاکت بسته مینشیند و هم روی تکتک پیامهای داخلش. همین یک فیلد است که اجازه میدهد Collector ساعت اشتباه دستگاه را تصحیح کند: اختلافی که بین sent_at و زمان دریافت خودش اندازه میگیرد، روی زمان رویدادها هم اعمال میشود. گوشیای که تاریخش دو سال جلوتر است باز هم داده قابل استفاده میدهد.
sent_at به جلوی پیام از قبل کدشده تزریق میشود، نه اینکه پیام دوباره کد شود. پیام شاید روزها پیش کد شده باشد و کد کردن دوبارهاش یعنی parse کردنش، که این ماژول عمدا بلد نیست. تزریق یک فیلد شناختهشده دقیق است، چون نویسنده خودمان همیشه {"type": را اول میگذارد و بعد از آکولاد فاصله نمیگذارد.
nullها و مپهای خالی اصلا فرستاده نمیشوند. بستهای از بیست رویداد که هرکدام شش آبجکت خالی حمل کنند، روی خط دیتای اندازهگیریشده بدنه بزرگتری است، و پول آن دیتا را مشتری میدهد نه ما.
این بدنه یک نمونه ساختگی نیست. همان بایتهایی است که یک دستگاه اندروید ۱۵ در تست آفلاین SDK روی سیم گذاشت و بهعنوان فایل طلایی در مخزن نشسته است:
{
"sent_at": "2026-08-07T11:01:47.885Z",
"batch": [
{
"sent_at": "2026-08-07T11:01:47.885Z",
"type": "track",
"message_id": "ff6447a2-6cc3-48b2-a429-f83cb07e126d",
"timestamp": "2026-08-07T11:01:10.609Z",
"anonymous_id": "711faad0-317b-40aa-81d7-253a39280348",
"event": "scripted_event",
"properties": { "index": 10, "note": "رویداد آزمایشی" },
"context": {
"library": { "name": "segmentic-android", "version": "0.1.0" },
"session_id": "4b3754ac-66cf-4ecf-a700-fc095072c8e5",
"app": { "name": "net.segmentic.sample", "version": "0.1.0" },
"os": { "name": "android", "version": "15" },
"device": {
"type": "android",
"manufacturer": "Google",
"model": "sdk_gphone64_x86_64"
},
"screen": { "width": 320, "height": 640, "density": 1 },
"locale": "en-US",
"timezone": "Asia/Tehran"
}
}
]
}
پنج نوع پیام وجود دارد: track، identify، screen، alias و page. مقدار page در enum سیم هست ولی هیچ متد عمومیای روی اندروید آن را نمیفرستد؛ page مال وب است.
context.library و context.session_id همیشه هستند و زیر کانتکست پلتفرم merge میشوند، نه رویش. یک جمعکننده پلتفرمی نباید بتواند نام کتابخانهای را که پیام را فرستاده عوض کند. این تست خصمانه دارد: یک platformContext که عمدا library و session_id جعلی برمیگرداند، و تست تأیید میکند هیچکدام روی سیم نمیرسد.
کانتکستی که خودکار جمع میشود، وقتی autoContext روشن است: app (نام بسته و نسخه)، os، device (سازنده و مدل)، screen (پیکسل و density)، locale، timezone و، فقط اگر اپ شما ACCESS_NETWORK_STATE را داشته باشد، network.
زمانها با محاسبه از epoch ساخته میشوند نه با SimpleDateFormat. آن کلاس thread safe نیست و این تابع از روی هر تردی صدا زده میشود که مشتری track() را رویش صدا زده باشد. یک نمونه مشترک زیر بار زمانهای درهم تولید میکند، که همان نوع باگی است که فقط در پروداکشن ظاهر میشود و شبیه مشکل سرور به نظر میرسد.
نویسنده JSON دستنویس است، با سقف عمق ۳۲. NaN و بینهایت بهجای بدنه نامعتبر، null میشوند. متن فارسی بدون escape عبور میکند و newline همیشه escape میشود، که همان چیزی است که قالب خطی صف رویش سوار است.
توکن پوش را اپ شما میدهد
این همان چیزی است که معمولا یک ساعت از وقت یک توسعهدهنده را میگیرد، پس صریح مینویسیم: این SDK توکن پوش را نمیگیرد. شما آن را دستش میدهید.
دلیلش تصمیمی است که ارزش دانستن دارد. اپی که پوش میفرستد از قبل فایربیس، یا بازار، یا مایکت را با پروژه خودش و نسخه خودش وصل کرده. اگر ما هم توکن میگرفتیم، یعنی این SDK داشت نسخه فایربیس را بهجای شما انتخاب میکرد و با نسخه خودتان تصادم میکرد. پس شما همان چیزی را که onNewToken خودتان داده به ما میدهید.
کل سمت مشتری نه خط است:
package com.example.shop
import com.google.firebase.messaging.FirebaseMessagingService
import net.segmentic.sdk.PushTransport
import net.segmentic.sdk.android.Segmentic
class MyMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
Segmentic.registerDevice(mapOf(PushTransport.FCM to token))
}
}
هر بار که ارائهدهنده توکن را میچرخاند دوباره صدایش بزنید. ثبت دوباره یک دستگاه تکراری نیست: سرور روی device_id بالا مینویسد. حالت چرخش همان است که اهمیت دارد، چون توکنی که عوض میشود و دوباره ثبت نمیشود یعنی کاربری که بیصدا از هر کمپینی میافتد بیرون، و تنها نشانهاش نرخ تحویلی است که در طول ماهها آرام پایین میرود.
نام مسیرها را دقیقا همینطور بنویسید. آنها از push.Transport در کد Go برداشته شدهاند:
object PushTransport {
const val FCM = "fcm"
const val BAZAAR = "bazaar"
const val MYKET = "myket"
const val MQTT = "mqtt"
}
فرستادن هر چیز دیگری غلط املاییای نیست که نادیده گرفته شود: سرور هشدار میدهد و توکن را میاندازد، و بعد مشتری کمپینی میبیند که هر ارسال را موفق گزارش میکند و هیچ چیزی تحویل نمیدهد. اگر مسیری بفرستید که به اندروید نمیرسد، مثلا apns، هشدار transport_not_supported در پاسخ میآید.
MQTT را نفرستید. ثابتش در هر دو طرف هست و سرور روی یک ثبت اندروید قبولش میکند، ولی هیچ ارائهدهندهای برای آن پیاده نشده و هیچ پیامی از آن مسیر بیرون نمیرود. امروز فقط fcm و bazaar و myket فرستنده دارند. توکنی که فقط mqtt باشد بدون هیچ هشداری ذخیره میشود و هرگز تحویل نمیگیرد، که بدترین حالت است: نه خطایی، نه هشداری، فقط سکوت.
چند مسیر روی یک دستگاه پشتیبانی میشود و سرور تصمیم میگیرد کدام تحویل بدهد. گوشیای که بدون Play Services فروخته شده باز هم بازار دارد:
Segmentic.registerDevice(
mapOf(
PushTransport.FCM to fcmToken,
PushTransport.BAZAAR to bazaarToken,
),
)
اگر اپ شما از قبل play-services-base دارد، جواب قطعی را خودتان بدهید. جستوجوی خود SDK فقط برای این است که SDK به هیچ وابستگی گوگل نیاز نداشته باشد:
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
val gms = GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
Segmentic.registerDevice(mapOf(PushTransport.FCM to fcmToken), hasGms = gms)
ثبت دستگاه
POST {apiHost}/v1/devices، با همان هدر Authorization: Bearer wk_seg_....
فیلدها، به ترتیبی که SDK مینویسد:
| فیلد JSON | همیشه هست | از کجا |
|---|---|---|
device_id | بله | segmentic_install_id، یک UUID تصادفی در حافظه خصوصی اپ |
platform | بله، همیشه "android" | ثابت |
user_id | فقط اگر کاربر واردشده باشد | خود SDK پر میکند |
anonymous_id | فقط اگر شناخته شده باشد | خود SDK پر میکند |
tokens | فقط اگر خالی نباشد | شما |
has_gms | فقط اگر بدانیم | جستوجوی بسته یا مقداری که شما دادید |
push_enabled | فقط اگر بدانیم | NotificationManager.areNotificationsEnabled() |
app_version | اگر خواندنی باشد | PackageManager |
manufacturer | بله | Build.MANUFACTURER |
model | بله | Build.MODEL |
os_name | بله، همیشه "android" | ثابت |
os_version | بله | Build.VERSION.RELEASE |
locale | بله | Locale.getDefault().toLanguageTag() |
timezone | بله | TimeZone.getDefault().id |
sdk_name | بله، همیشه "segmentic-android" | ثابت |
sdk_version | بله، همیشه "0.1.0" | ثابت |
هویت را خود SDK پر میکند، نه صداکننده. پس اپ میزبان نمیتواند دستگاهی را بهنام کاربری ثبت کند که از آن حساب خارج شده است.
بدنه واقعی، از همان اجرای شبیهساز:
{
"device_id": "79a1c2c3-a61a-4816-a355-f3d5a0c7ffc2",
"platform": "android",
"anonymous_id": "711faad0-317b-40aa-81d7-253a39280348",
"tokens": { "fcm": "scripted-token-not-a-real-one" },
"has_gms": true,
"push_enabled": false,
"app_version": "0.1.0",
"manufacturer": "Google",
"model": "sdk_gphone64_x86_64",
"os_name": "android",
"os_version": "15",
"locale": "en-US",
"timezone": "Asia/Tehran",
"sdk_name": "segmentic-android",
"sdk_version": "0.1.0"
}
و پاسخ موفق:
{ "status": "ok" }
اگر توکنی مسیر اشتباهی داشته باشد، پاسخ باز هم 200 است ولی هشدار میآورد و توکن سالم ذخیره میشود:
{
"status": "ok",
"warnings": [
{
"code": "transport_not_supported",
"field": "apns",
"message": "transport apns cannot deliver to android"
}
]
}
SDK این هشدارها را در logcat مینویسد، حتی وقتی پاسخ موفق است. سکوت اینجا همان چیزی است که یک نصب خراب را به کمپینی تبدیل میکند که صد درصد ارسال گزارش میدهد.
has_gms و push_enabled سهحالتهاند و وقتی نمیدانیم اصلا فرستاده نمیشوند. نه false. سرور «نبودن» را «نامعلوم» میخواند و نامعلوم با false یکی نیست. فرستادن false جایی که فقط نگاه نکردهایم، کاربری را ساکت میکند که خودش چیزی نخواسته بود، و تنها نشانهاش مخاطبی است که آرام کوچک میشود.
has_gmsوقتی بسته Play Services پیدا شودtrueاست، رویNameNotFoundException(که یک گوشی معمولی ایرانی است، نه خطا)falseاست، و روی هر استثنای دیگریnull.push_enabledمقدارareNotificationsEnabled()است، وnullوقتی که اصلا نمیشود بهNotificationManagerرسید.
نتیجه ثبت سه حالت دارد، و SDK هر سه را جدا میکند:
| نتیجه | کی | روی دیسک چه میشود |
|---|---|---|
REGISTERED | 2xx | رکورد معلق پاک میشود |
REFUSED | 4xx بجز 429 | رکورد معلق پاک میشود. همان بدنه در هر اجرا به همان شکل رد میشود، پس تلاش دوباره حلقهای است که نه تمام میشود نه جواب میدهد |
PENDING | 429، 5xx، یا کد 0 | بدنه در segmentic_pending_device نوشته میشود و در هر flush و هر اجرای بعدی دوباره فرستاده میشود |
چرا ثبت دستگاه دوباره تلاش میشود در حالی که رویداد صف دارد و این ندارد: خود Collector در کامنت کدش نوشته که SDK باید دوباره تلاش کند، چون هیچکس دیگری این کار را نمیکند. رویدادی که دیر برسد باز هم همان رویداد است، ولی توکنی که هرگز نمیرسد یعنی کسی که موافقت کرده اعلان بگیرد و هیچوقت نمیشود به او رسید.
device_id عمدا نه شناسه تبلیغاتی است و نه Settings.Secure.ANDROID_ID. هر دوی آنها یک نفر را در اپهای بیربط شناسایی میکنند، که سؤال حریم خصوصیای است که مشتری باید جوابش را بدهد نه ما، و گوگل هم اولی را محدود کرده است. این یک مقدار تصادفی در حافظه خصوصی خود اپ است: تا حذف اپ یا پاک کردن دادهاش میماند و هیچجای دیگری دنبال کاربر نمیرود.
مسیر ثبت دستی، برای هر پلتفرمی که SDK ندارد، در ثبت دستگاه است.
پیام درونبرنامهای
بنر و مودالی که در پنل ساخته میشود خودکار در اپ کشیده میشود. تنها کاری که از شما خواسته میشود این است:
Segmentic.screen("cart")
بازدید صفحه همان لحظهای است که پیام درونبرنامهای تصمیم گرفته میشود، چون نام صفحه همان چیزی است که قاعده هدفگیری با آن مطابقت میکند. هیچ گزینهای برای خاموش کردنش نیست.
بقیهاش خودش انجام میشود، روی سه ترد: گرفتن فهرست روی ترد شبکه، تصمیم که یک خواندن کوچک دیسک است روی همان ترد، و کشیدن روی ترد اصلی با تأخیری که خود کمپین گفته.
گرفتن فهرست. GET {apiHost}/v1/onsite?write_key=... با هدر Authorization هم. کلید در هر دو جا هست، عمدا: شکل query چیزی است که پاسخ را برای یک CDN که به هدر Authorization کاری ندارد قابل کش میکند. فهرست ۶۰ ثانیه معتبر است، همان عددی که Collector در هدر Cache-Control میگذارد. شکست کاملا بیصداست و فهرست قبلی سر جایش میماند: این کد داخل اپ کس دیگری اجرا میشود و خرابی ما باید به «امروز پیامی نیست» تنزل کند، نه به خطایی که کاربر او میبیند.
هدفگیری روی دستگاه سنجیده میشود، نه روی سرور. جایگزینش یک درخواست بهازای هر بازدید صفحه است، روی مسیر حیاتی اپ مشتری، با تأخیر ما جلوی محتوای او و در دسترس بودن ما جلوی کسبوکار او.
قاعدهها دقیقا همانهای SDK وباند و به همان ترتیب اجرا میشوند، با این تفاوتها روی گوشی:
| قاعده سمت سرور | روی اندروید با چه مطابقت میکند |
|---|---|
targeting.url_contains | نام صفحهای که به screen() دادهاید |
targeting.url_not_contains | همان |
targeting.devices | فقط mobile یا tablet |
targeting.delay_seconds | تأخیر پیش از کشیدن، محدودشده به بازه صفر تا ۶۰ ثانیه |
targeting.new_visitors_only | آیا این اولین اجرای این نصب است |
targeting.returning_only | برعکس بالا |
targeting.logged_in | سهحالته: نبودنش یعنی «فرقی نمیکند» |
targeting.traits | ویژگیهای آخرین identify که مقدارشان اسکالر بوده |
targeting.scroll_percent | پشتیبانی نمیشود، بیصدا نادیده گرفته میشود |
targeting.on_exit_intent | پشتیبانی نمیشود، بیصدا نادیده گرفته میشود |
تطبیق همیشه زیررشتهای است و هرگز regex نیست: الگویی که یک بازاریاب مینویسد الگویی است که میتواند فاجعهبار کند باشد، و این روی هر صفحه اپ کس دیگری اجرا میشود.
کلاس دستگاه از smallestScreenWidthDp میآید و مرزش ۶۰۰ است. کمتر از آن mobile و از آن به بالا tablet. روی اندروید desktop وجود ندارد. توجه کنید که این با SDK وب فرق دارد، که مرزهایش ۷۶۸ و ۱۰۲۴ پیکسل CSS است. ۶۰۰ مرز خود اندروید برای همین سؤال است، پس کمپین همانطور رفتار میکند که layoutهای خود اپ.
سقف تکرار به این ترتیب اعمال میشود و هر شرط قبلی بر بعدی مقدم است: خارج از بازه starts_at و ends_at؛ سپس «تبدیل شده»، که برای همیشه جلویش را میگیرد؛ سپس «بسته شده» بهشرط اینکه کمپین قابل بستن باشد؛ سپس max_impressions؛ سپس cooldown_hours. مقدار صفر در دو تای آخر یعنی بیسقف.
سقف هم روی دستگاه اعمال میشود و هم روی سرور، و باید هم بشود: فقط حافظه محلی یعنی پاک کردن داده اپ مودال را بیسقف میکند، و فقط سرور یعنی یک درخواست بهازای هر بازدید صفحه، که کل این طراحی برای اجتناب از آن وجود دارد.
چه چیزی واقعا کشیده میشود. رندرر با Viewهای ساده کار میکند، بدون Compose و بدون XML، چون وابستگی Compose اینجا یعنی وابستگی Compose در بیلد هر مشتری، از جمله آنهایی که هنوز روی View هستند، و یعنی انتخاب نسخه Compose بهجای آنها.
| نوع کمپین | روی اندروید |
|---|---|
banner | کشیده میشود |
modal | کشیده میشود |
slidein | کشیده میشود، به شکل بنر. انیمیشنش هنوز مشخص نشده |
survey | کشیده نمیشود |
نظرسنجی ویجت ورودی و یک جریان سؤال میخواهد. رندرر برای نوعی که نمیشناسد false برمیگرداند و کمپین آنوقت نه سقف میخورد و نه گزارش میشود: وقتی رندرر آن نوع را یاد گرفت، همان کمپین هنوز به کاربر بدهکار است نه اینکه بیصدا سوخته باشد.
هسته یک متد respond برای فرستادن جواب نظرسنجی دارد و تست هم دارد، ولی هیچ چیزی در لایه اندروید صدایش نمیزند. تا وقتی رندرر نظرسنجی نوشته نشده، این متد فقط برای کسی در دسترس است که خودش یک OnsiteManager بسازد.
پیام روی content root خود Activity اضافه میشود، نه در یک Dialog. یک Dialog پنجره خودش را میگیرد، که روی اندروید یعنی به شکلهایی که هیچکس نمیخواهد از Activity صاحبش عمر بیشتری میکند، و با کیبورد جابهجا نمیشود و insetهای اپ را رعایت نمیکند.
جزئیاتی که در عمل به آنها برمیخورید:
- هر بار فقط یک پیام.
show()اولremove()را صدا میزند. - متن بدنه در سه خط با «...» بریده میشود. بازاریاب بالاخره یک انشا paste میکند، و بنری که رشد میکند تا کل اپ را بپوشاند از بنر بریده بدتر است.
- بنر پیشفرض پایین مینشیند، مگر
content.positionبرابرtopباشد. بنر روی نوار ابزار خود اپ ناوبری را میپوشاند، و کاربری که نمیتواند ناوبری کند اپ را میبندد. - دکمه بستن حداقل ۴۸ dp سطح لمس دارد و
contentDescriptionآن «بستن» است. - رنگهای پیشفرض: پسزمینه
#1F2430، متن#F5F7FA، رنگ تأکید#2F6FED. رنگی که parse نشود به پیشفرض برمیگردد و throw نمیکند: بازاریابی که اسم یک رنگ را در فیلد hex تایپ میکند نباید اپی را که در آن تبلیغ میکند بشکند. - scrim مودال حتی وقتی کمپین قابل بستن نیست هم clickable است، تا یک لمس از پشت مودالی که رویش نشسته به اپ نیفتد.
- کلیک قبل از باز شدن لینک گزارش میشود. لینکی که باز نمیشود باز هم کلیکی است که مشتری باید در گزارشش ببیند.
یک ایراد واقعی که فقط با نگاه کردن به عکس صفحه پیدا شد و ارزش نوشتن دارد: نسخه اول بنر را چسبیده به لبه پایین میگذاشت و نوار حرکتی خط آخر را میخورد. دو تلاش با inset listener شکست خورد، چون listener روی ویویی که هنوز attach نشده هرگز صدا زده نمیشود و بعد از attach هم به این بستگی دارد که هر والد insetها را پایین بدهد، که یک کتابخانه نمیتواند درباره سلسلهمراتب ویوی کس دیگری فرضش کند. راهحل، خواندن مستقیم rootWindowInsets در همان لحظه کشیدن است. این از قبل هم مهم بود و حالا مهمتر است، چون اندروید ۱۵ هر اپی با targetSdk برابر ۳۵ را edge to edge میکند.
گزارش. POST {apiHost}/v1/onsite/event با بدنهای به این شکل، که action یکی از impression، dismiss، click یا convert است و user_id وقتی کاربر ناشناس است اصلا نمیآید:
{
"campaign_id": 42,
"action": "click",
"anonymous_id": "711faad0-317b-40aa-81d7-253a39280348",
"user_id": "u_123"
}
پیامد محلی قبل از درخواست اعمال میشود. سقف باید حتی وقتی این درخواست هرگز نمیرسد هم برقرار بماند، چون جایگزینش مودالی است که برای کسی که سیگنال ندارد در هر اجرا دوباره ظاهر میشود.
اگر خودتان بخواهید پیام را از صفحه بردارید:
Segmentic.dismissOnsite()
مفهوم کلی و ساختن کمپین در پنل در پیام درونسایتی است.
انصراف از ردیابی
Segmentic.optOut()
Segmentic.optIn()
val stopped = Segmentic.isOptedOut()
optOut() سه کار میکند: پرچم را میگذارد و در segmentic_opt_out روی دیسک ذخیرهاش میکند، صف را خالی میکند، و ثبت دستگاه معلق را هم پاک میکند.
خالی کردن صف نکته اصلی است. احترام گذاشتن به انصراف فقط برای رویدادهای آینده، در حالی که بیسروصدا آنچه از قبل جمع شده تحویل داده شود، انصراف نیست.
بعد از انصراف: هیچ رویدادی صف نمیشود، flush() بدون هیچ درخواستی برمیگردد، registerDevice بدون هیچ درخواستی REFUSED میدهد، و پیام درونبرنامهای کشیده نمیشود. پرچم بعد از بستن و باز کردن اپ هم میماند، چون روی دیسک است.
معادل Do Not Track روی اندروید وجود ندارد، درست هم هست: چنین سیگنالی در سیستمعامل نیست. سیاست رضایت و آنچه پنل با آن میکند در رضایت است.
چه چیزی امروز نیست
فهرست صادقانه، تا کسی یک بعدازظهر را صرف گشتن دنبال چیزی نکند که وجود ندارد:
page(). روی اندروید وجود ندارد.screen()معادلش است.- انتساب کمپین. روی وب یک کلیک با
sg_midگرفته میشود، یکmessage_clickedمیسازد و هفت روز روی رویدادهای بعدی سوار میشود. روی اندروید هیچکدام از اینها نیست. تبدیل موبایل امروز به یک پیام نسبت داده نمیشود. - کمکی برای گرفتن توکن پوش. فقط
registerDevice، و توکن را شما میدهید. - رندر نظرسنجی، و هر صداکنندهای برای
OnsiteManager.respondدر لایه اندروید. - تریگرهای عمق اسکرول و قصد خروج. روی وب هستند، اینجا نه.
- بازار و مایکت با توکن واقعی. نگاشت توکنشان پذیرفته میشود و سرور مسیرشان را میشناسد، ولی هیچکدام هیچوقت با توکن واقعی امتحان نشده. اگر روی این دو فروشگاه پوش میفرستید، اولین نفرید.
- انتشار روی مخزن. پکیج روی هیچ مخزن عمومیای نیست؛ برای گرفتنش با ما تماس بگیرید.
- اثبات روی یک اپ مشتری واقعی. اثبات موجود روی اپ نمونهی خودمان است. بخش چه چیزی اثبات شده و چطور میگوید دقیقا چه چیزی پوشش دارد.
چه چیزی ثابت شده و چطور
۱۰۳ تست کاتلین، همه روی JVM ساده:
| فایل | تعداد | چه چیزی |
|---|---|---|
SegmenticClientTest.kt | ۳۳ | شکل سیم، حذف تکراری، رفتار 400 و 429 و 503 و بیسیگنال، هویت، alias، انصراف، سرریز بافر، ثبت دستگاه و سه نتیجهاش، نرمالسازی تنظیمات، و تست خصمانه کانتکست پلتفرم |
CoreTest.kt | ۲۲ | نویسنده JSON، تاریخ ISO 8601، صف، نشست، backoff |
OnsiteTest.kt | ۲۲ | هدفگیری، سقف تکرار، ذخیره رکورد دیدهشده، کلاس دستگاه |
OnsiteManagerTest.kt | ۲۰ | گرفتن فهرست، parse، کش، تصمیم، گزارش، و خواننده JSON |
HttpIntegrationTest.kt | ۶ | روی یک سوکت واقعی، مقابل com.sun.net.httpserver.HttpServer روی localhost |
۶ تست Go روی بایتهای واقعی. فایلهای backend/internal/collector/testdata/android-sdk/ فیکسچری نیستند که کسی نوشته باشد تا با parser بخواند. همان بایتهایی هستند که یک دستگاه اندروید ۱۵ روی سیم گذاشت: دوازده رویداد در حالی که هیچکس روی پورت گوش نمیداد بافر شد، اپ با force-stop کشته شد، و بعد از راهاندازی دوباره تحویل شدند. یک Collector ضبطکننده درخواستها را عینا نگه داشت.
آن شش تست اینها را تأیید میکنند:
- بسته اول دقیقا پنج پیام دارد، هر پیام از
model.Normalizeواقعی با صفر هشدار رد میشود، وsdk_nameوsdk_versionوos_nameوos_versionهماناند که باید. properties.indexبهشکل عدد میرسد نه رشته. اگر SDK صفر را متن فرستاده بود، نگاشت عددی هیچ ورودیای نداشت و هر سگمنت «بزرگتر از» روی آن ویژگی بیصدا هرگز مطابقت نمیکرد. وproperties.noteبرابر «رویداد آزمایشی» است، یعنی فارسی از کدگذاری کاتلین و دیسک و مرگ پروسه و سوکت و parse شدن در Go سالم بیرون آمده.- فاصله بین زمان اولین رویداد و
sent_atبسته بیشتر از سی ثانیه و کمتر از ده دقیقه است، پس تصحیح ساعت واقعا کاری برای انجام دادن دارد. - دوازده رویداد با
batchSizeبرابر پنج، دم دقیقا دوتایی میگذارد. دم یکتایی یا سهتایی یعنیpeekیاackصف اشتباه است. - بدنه طلایی ثبت دستگاه با صفر هشدار نرمال میشود و توکن
fcmسر جایش میرسد. has_gmsوpush_enabledهر دو بهشکل اشارهگر میرسند، یعنی حالت سهگانه تا خود سرور حفظ شده.
اجرای دستی روی شبیهساز اندروید ۱۵، که مخزن دوباره اجرایش نمیکند ولی سه دستور adb تکرارش را ممکن میکند:
adb shell am start -n net.segmentic.sample/.MainActivity --ei fire 12
adb shell am force-stop net.segmentic.sample
adb shell am start -n net.segmentic.sample/.MainActivity --ez flush true
هر ۱۲ رویداد در بستههای پنج و پنج و دو رسیدند، با ۱۲ message_id متفاوت و هیچکدام دو بار.
برای سنجیدن پیام درونبرنامهای، اکسترا sgscreen است نه screen، چون am start نام ساده را برای خودش برمیدارد و بیصدا میبلعد:
adb shell am start -n net.segmentic.sample/.MainActivity --es sgscreen cart
پوش واقعی، روی همان شبیهساز با یک پروژه فایربیس دورانداختنی: فایربیس یک توکن ۱۴۲ کاراکتری داد، onNewToken آن را به SDK سپرد، SDK روی POST /v1/devices ثبتش کرد و یک پیام از FCM v1 فرستاده شد. با اپ در پیشزمینه به onMessageReceived رسید و با اپ در پسزمینه سیستم خودش نوتیف را کشید، هر دو با تیتر و متن فارسی سالم.
نکتهای که همانجا خودش را نشان داد: پیش از دادن اجازه اعلان، push_enabled مقدار false رفت و بعد از pm grant شد true. یعنی حالت سهگانه واقعیت سیستم را گزارش میکند نه فرضی را که ما کرده باشیم.
پیام درونبرنامهای، مقابل Collectorی که یک کمپین زنده سرو میکرد: بنر با تیتر و متن فارسی کشیده شد، گزارش impression روی POST /v1/onsite/event رسید، و در اجرای دوم سقف تکرار جلویش را گرفت.