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

SDK اندروید

SDK رسمی اندروید برای ثبت رویداد، هویت، پوش و پیام درون‌برنامه‌ای.

SDK اندروید رویداد ثبت می‌کند، هویت را تشخیص می‌دهد، پوش می‌گیرد و پیام درون‌برنامه‌ای را می‌کشد. سه ماژول Gradle است، به‌علاوه یک اپ نمونه که دقیقا همان‌طور صدایشان می‌زند که اپ شما می‌زند.

#چطور به بیلد شما می‌رسد

این SDK به صورت سورس و از روی مخزن می‌آید، نه از یک مخزن بسته. یک بار اضافه‌اش می‌کنید و بعد همان مختصات همیشگی را می‌نویسید:

Kotlin
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 خودش مختصات را با پروژه محلی جایگزین می‌کند.

settings.gradle.kts (اپ شما)
includeBuild("../segmentic/sdk/android")
app/build.gradle.kts
dependencies {
    implementation("net.segmentic:segmentic-android:0.1.0")
}

#مسیر دو: انتشار محلی

یک بار در مخزن SDK اجرا کنید:

Shell
cd segmentic/sdk/android
./gradlew :segmentic-core:publishToMavenLocal :segmentic-android:publishToMavenLocal

چهار فایل در ~/.m2/repository/net/segmentic/ می‌نشیند. بعد در اپ خودتان mavenLocal() را اضافه کنید:

settings.gradle.kts (اپ شما)
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        mavenLocal()
    }
}
app/build.gradle.kts
dependencies {
    implementation("net.segmentic:segmentic-android:0.1.0")
}

هر دو ماژول یک sources.jar هم منتشر می‌کنند. این عمدی است: کامنت‌های داخل segmentic-core توضیح می‌دهند چرا صف از جلو افت می‌کند و چرا یک 4xx دور ریخته می‌شود، و کسی که نصفه‌شب دنبال مشکل اپ خودش می‌گردد باید بتواند همان را در IDE بخواند نه اینکه حدس بزند.

وقتی روزی مخزن راه بیفتد، دستور انتشارش این است و هیچ چیز دیگری در کد عوض نمی‌شود:

Shell
./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.aar31730
segmentic-android-0.1.0-sources.jar6705
segmentic-core-0.1.0.jar54948
segmentic-core-0.1.0-sources.jar19996

داخل AAR پنج ورودی است: AndroidManifest.xml، classes.jar، R.txt خالی، proguard.txt و یک فایل متادیتا. هیچ منبعی، دقیقا همان چیزی که فایل بیلد قول داده.

عددی که نداریم: هزینه واقعی SDK داخل اپ شما، یعنی تعداد متد یا اضافه‌شدن حجم dex بعد از R8. هیچ اندازه‌گیری‌ای از این در مخزن نیست. اندازه AAR و jar را داریم و همان را نوشتیم.

کف پلتفرم: minSdk = 24 (اندروید ۷)، compileSdk = 35، جاوا ۱۷ برای source و target. پایین‌تر رفتن یعنی وارد کردن قاعده‌های desugaring به بیلد شما، برای سهمی از دستگاه‌ها که حالا زیر یک درصد است.

#چه چیزی به منیفست شما اضافه می‌شود

دو چیز، و همین.

manifest کتابخانه، همان که با AAR merge می‌شود
<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:

MyApp.kt
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",
            ),
        )
    }
}

و در منیفست خودتان:

AndroidManifest.xml
<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 متفاوت ببیند.

گزینهنوعپیش‌فرضمعنی
writeKeyStringندارد، اجباریکلید نوشتن از پنل. عمومی است و می‌تواند داخل APK باشد
apiHostStringندارد، اجباریآدرس Collector، مثلا https://in.segmentic.net
batchSizeInt20ارسال به‌محض اینکه این تعداد پیام بافر شد
flushIntervalMsLong10_000حداکثر فاصله بین دو ارسال
maxQueueSizeInt500چند پیام حق دارند روی دیسک منتظر بمانند
maxRetriesInt10چند شکست پشت‌سرهم تا آهنگ تلاش مجدد از رشد بایستد
autoContextBooleantrueافزودن اپ و سیستم و صفحه و زبان و منطقه زمانی به هر پیام
sessionTimeoutMsLong30 * 60_000فاصله بی‌کاری که رویداد بعدی را وارد نشست تازه می‌کند
debugBooleanfalseلاگ در 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 معمولی دیده می‌شود.

Kotlin
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()

نمونه کامل، همان‌طور که یک اپ فروشگاهی صدایش می‌زند:

CartActivity.kt
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 است:

فیلدنوعچیست
queuedIntچند پیام همین حالا روی دیسک منتظرند
sentLongچند پیام Collector پذیرفته است
droppedLongچند پیام افتاده، از پر شدن صف یا از پر بودن دیسک یا از رد دائمی سرور
consecutiveFailuresIntچند شکست پشت‌سرهم
optedOutBooleanانصراف داده شده یا نه
durableStorageBooleanروی اندروید همیشه true است، چون FileStore استفاده می‌شود
anonymousIdStringشناسه ناشناس فعلی
userIdString?کاربر واردشده، یا null
devicePendingBooleanیک ثبت دستگاه هست که هنوز نرسیده و دوباره تلاش می‌شود

#تردها

این بخش همان چیزی است که مشتری حس می‌کند.

  • 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 روی سیم گذاشت و به‌عنوان فایل طلایی در مخزن نشسته است:

testdata/android-sdk/batch-tail.json
{
  "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 خودتان داده به ما می‌دهید.

کل سمت مشتری نه خط است:

MyMessagingService.kt
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 برداشته شده‌اند:

Kotlin
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 فروخته شده باز هم بازار دارد:

Kotlin
Segmentic.registerDevice(
    mapOf(
        PushTransport.FCM to fcmToken,
        PushTransport.BAZAAR to bazaarToken,
    ),
)

اگر اپ شما از قبل play-services-base دارد، جواب قطعی را خودتان بدهید. جست‌وجوی خود SDK فقط برای این است که SDK به هیچ وابستگی گوگل نیاز نداشته باشد:

Kotlin
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 پر می‌کند، نه صداکننده. پس اپ میزبان نمی‌تواند دستگاهی را به‌نام کاربری ثبت کند که از آن حساب خارج شده است.

بدنه واقعی، از همان اجرای شبیه‌ساز:

testdata/android-sdk/device.json
{
  "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"
}

و پاسخ موفق:

JSON
{ "status": "ok" }

اگر توکنی مسیر اشتباهی داشته باشد، پاسخ باز هم 200 است ولی هشدار می‌آورد و توکن سالم ذخیره می‌شود:

JSON
{
  "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 هر سه را جدا می‌کند:

نتیجهکیروی دیسک چه می‌شود
REGISTERED2xxرکورد معلق پاک می‌شود
REFUSED4xx بجز 429رکورد معلق پاک می‌شود. همان بدنه در هر اجرا به همان شکل رد می‌شود، پس تلاش دوباره حلقه‌ای است که نه تمام می‌شود نه جواب می‌دهد
PENDING429، 5xx، یا کد 0بدنه در segmentic_pending_device نوشته می‌شود و در هر flush و هر اجرای بعدی دوباره فرستاده می‌شود

چرا ثبت دستگاه دوباره تلاش می‌شود در حالی که رویداد صف دارد و این ندارد: خود Collector در کامنت کدش نوشته که SDK باید دوباره تلاش کند، چون هیچ‌کس دیگری این کار را نمی‌کند. رویدادی که دیر برسد باز هم همان رویداد است، ولی توکنی که هرگز نمی‌رسد یعنی کسی که موافقت کرده اعلان بگیرد و هیچ‌وقت نمی‌شود به او رسید.

device_id عمدا نه شناسه تبلیغاتی است و نه Settings.Secure.ANDROID_ID. هر دوی آن‌ها یک نفر را در اپ‌های بی‌ربط شناسایی می‌کنند، که سؤال حریم خصوصی‌ای است که مشتری باید جوابش را بدهد نه ما، و گوگل هم اولی را محدود کرده است. این یک مقدار تصادفی در حافظه خصوصی خود اپ است: تا حذف اپ یا پاک کردن داده‌اش می‌ماند و هیچ‌جای دیگری دنبال کاربر نمی‌رود.

مسیر ثبت دستی، برای هر پلتفرمی که SDK ندارد، در ثبت دستگاه است.

#پیام درون‌برنامه‌ای

بنر و مودالی که در پنل ساخته می‌شود خودکار در اپ کشیده می‌شود. تنها کاری که از شما خواسته می‌شود این است:

Kotlin
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 وقتی کاربر ناشناس است اصلا نمی‌آید:

JSON
{
  "campaign_id": 42,
  "action": "click",
  "anonymous_id": "711faad0-317b-40aa-81d7-253a39280348",
  "user_id": "u_123"
}

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

اگر خودتان بخواهید پیام را از صفحه بردارید:

Kotlin
Segmentic.dismissOnsite()

مفهوم کلی و ساختن کمپین در پنل در پیام درون‌سایتی است.

#انصراف از ردیابی

Kotlin
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 تکرارش را ممکن می‌کند:

Shell
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 نام ساده را برای خودش برمی‌دارد و بی‌صدا می‌بلعد:

Shell
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 رسید، و در اجرای دوم سقف تکرار جلویش را گرفت.

قبلیSDK وببعدیثبت دستگاه

در این صفحه

  • چطور به بیلد شما می‌رسد
  • سه ماژول
  • افزودن به اپ
  • بدون وابستگی، و چه چیزی می‌خرد
  • چه چیزی به منیفست شما اضافه می‌شود
  • راه‌اندازی
  • تنظیمات
  • متدهای عمومی
  • تردها
  • صف آفلاین
  • آنچه روی سیم می‌رود
  • توکن پوش را اپ شما می‌دهد
  • ثبت دستگاه
  • پیام درون‌برنامه‌ای
  • انصراف از ردیابی
  • چه چیزی امروز نیست
  • چه چیزی ثابت شده و چطور

سگمنتیک

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