{"id":"biotoxin495/analyticskit","name":"analyticskit","scope":"biotoxin495","platform":"roblox","description":"A standalone, server-only wrapper around Roblox AnalyticsService.","version":"1.0.2","latest":"1.0.2","versions":["1.0.0","1.0.1","1.0.2"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"abca64aaad091295956c02638dfc26d04cf915a48d447937b9bea2537fab15f9","likes":0,"downloads":0,"install":"forest install biotoxin495/analyticskit","url":"https://forest.dev/p/roblox/biotoxin495/analyticskit","files":"https://api.forest.dev/ai/package/roblox/biotoxin495/analyticskit/files","readme":"# AnalyticsKit — A standalone server-only wrapper for Roblox's AnalyticsService\n\n**AnalyticsKit**, a standalone, server-only wrapper around Roblox's `AnalyticsService`.\n\nCalling `AnalyticsService` directly from scattered locations in a codebase tends to get messy: payloads go unvalidated, custom fields get encoded inconsistently, Studio calls silently vanish into the void, and repetitive economy events (passive income ticks, per-hit rewards) flood the dashboard with one request per tick.\n\n**AnalyticsKit** addresses these pain points. It centralizes analytics calls, validates event payloads, provides Studio-friendly diagnostics, supports declarative event catalogs, and can batch repetitive economy events without merging unrelated SKUs.\n\n## 🚀 Features\n\n- Direct wrappers for custom, economy, onboarding, funnel, and progression events\n- Declarative registered events through `RegisterEvent()` and `LogEvent()`\n- Economy batching that preserves player, flow, currency, transaction type, SKU, and custom fields\n- Per-SKU batching rules and explicit per-call overrides\n- Automatic custom-field conversion to Roblox's three supported analytics fields\n- Input validation with warning or strict-error modes\n- Studio print, record, or ignore behavior\n- Injectable transport for tests and custom inspection\n- Funnel-step deduplication\n- Estimated AnalyticsService rate-budget tracking\n- Event lifecycle signals and diagnostic counters\n- Player-leave flushing and explicit cleanup\n- No Kernel, Promise, Signal, or framework dependency\n\n## 🛠️ Installation\n\nPlace `AnalyticsKit.luau` in a server-accessible package location, such as:\n\n```text\nReplicatedStorage\n└── Packages\n    └── AnalyticsKit\n```\n\nRequire and construct it from a server Script or server ModuleScript:\n\n```luau\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal AnalyticsKit = require(ReplicatedStorage.Packages.AnalyticsKit)\n\nlocal Analytics = AnalyticsKit.new()\n```\n\nAnalyticsKit must be constructed on the server.\n\n## 📖 Quick start\n\n```luau\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal AnalyticsKit = require(ReplicatedStorage.Packages.AnalyticsKit)\n\nlocal Analytics = AnalyticsKit.new({\n\tStudioBehavior = \"Print\",\n\n\tEconomyTransactionTypes = {\n\t\tPassiveIncome = Enum.AnalyticsEconomyTransactionType.Gameplay,\n\t\tDailyReward = Enum.AnalyticsEconomyTransactionType.TimedReward,\n\t\tCoinPack = Enum.AnalyticsEconomyTransactionType.IAP,\n\t},\n\n\tEconomyBatching = {\n\t\tEnabled = true,\n\t\tInterval = 5,\n\t\tDefault = false,\n\t\tSKUs = {\n\t\t\tPassiveIncome = true,\n\t\t},\n\t},\n})\n\nAnalytics:LogCustomEvent(player, \"Quest_Completed\", 1)\n\nAnalytics:LogEconomyEvent(\n\tplayer,\n\tEnum.AnalyticsEconomyFlowType.Source,\n\t\"Coins\",\n\t1,\n\t501,\n\tnil,\n\t\"PassiveIncome\"\n)\n```\n\nWhen `transactionType` is `nil`, AnalyticsKit checks `EconomyTransactionTypes[itemSku]`, then falls back to `DefaultEconomyTransactionType`.\n\n## Custom fields\n\nRoblox analytics supports up to three custom fields. `CreateCustomFields()` converts strings, numbers, and booleans into the expected dictionary:\n\n```luau\nlocal fields = AnalyticsKit.CreateCustomFields(\n\t\"Warrior\",\n\t12,\n\ttrue\n)\n\nAnalytics:LogCustomEvent(player, \"Mission_Completed\", 90, fields)\n```\n\nThis produces:\n\n```luau\n{\n\t[Enum.AnalyticsCustomFieldKeys.CustomField01.Name] = \"Warrior\",\n\t[Enum.AnalyticsCustomFieldKeys.CustomField02.Name] = \"12\",\n\t[Enum.AnalyticsCustomFieldKeys.CustomField03.Name] = \"true\",\n}\n```\n\nDirect dictionaries using the Roblox enum keys or their `.Name` strings are also accepted and normalized.\n\n## Economy batching\n\nAnalyticsKit batches only economy events that resolve to the same complete analytics identity:\n\n- Player\n- Economy flow type\n- Currency type\n- Transaction type\n- Item SKU\n- Custom-field values\n\nFor example, these calls can become one request:\n\n```luau\nAnalytics:LogEconomyEvent(\n\tplayer,\n\tEnum.AnalyticsEconomyFlowType.Source,\n\t\"Coins\",\n\t1,\n\t101,\n\tnil,\n\t\"PassiveIncome\"\n)\n\nAnalytics:LogEconomyEvent(\n\tplayer,\n\tEnum.AnalyticsEconomyFlowType.Source,\n\t\"Coins\",\n\t1,\n\t102,\n\tnil,\n\t\"PassiveIncome\"\n)\n```\n\nThe flushed event reports an amount of `2`, the latest ending balance of `102`, and the unchanged SKU `PassiveIncome`.\n\nA different SKU is always stored in a different batch:\n\n```text\nPassiveIncome != DailyReward != QuestReward\n```\n\nThis avoids synthetic combined SKU names and preserves source/sink attribution in the Roblox analytics dashboard.\n\n### Recommended batching policy\n\nBatch repetitive, low-value events such as:\n\n- Passive income ticks\n- Repeated resource pickups\n- Rapid low-value sales\n- Per-hit or per-tick resource rewards\n\nDo not normally batch singular events such as:\n\n- In-app purchases\n- Area unlocks\n- Upgrades\n- Major rewards\n- One-time purchases\n\nBatching is opt-in by default:\n\n```luau\nEconomyBatching = {\n\tEnabled = true,\n\tInterval = 5,\n\tDefault = false,\n\tSKUs = {\n\t\tPassiveIncome = true,\n\t\tStickSold = true,\n\t},\n}\n```\n\nTo batch every SKU unless specifically disabled:\n\n```luau\nEconomyBatching = {\n\tDefault = true,\n\tSKUs = {\n\t\tCoinPack = false,\n\t\tAreaUnlock = false,\n\t},\n}\n```\n\nA call can override the configured policy with the final `batch` argument:\n\n```luau\nAnalytics:LogEconomyEvent(\n\tplayer,\n\tEnum.AnalyticsEconomyFlowType.Source,\n\t\"Coins\",\n\t5,\n\t205,\n\tnil,\n\t\"TemporaryReward\",\n\tnil,\n\ttrue\n)\n```\n\nWhen a direct, non-batched economy event is logged, AnalyticsKit flushes that player's existing economy batches first by default. This reduces event reordering around important transactions. Set `FlushEconomyBatchesBeforeDirect = false` to disable that behavior.\n\n### Batching trade-off\n\nBatching preserves total amounts and SKU identity, but it intentionally reduces transaction count. It also retains only the latest ending balance for the batch. Use it for repetitive events where reduced request volume matters more than one-row-per-transaction reporting.\n\n## ⚙️ Direct API\n\n### Custom events\n\n```luau\nAnalytics:LogCustomEvent(\n\tplayer,\n\t\"Minigame_Run_Completed\",\n\t42.5,\n\tAnalyticsKit.CreateCustomFields(\"Obby\", true)\n)\n```\n\n```luau\nLogCustomEvent(\n\tplayer: Player,\n\teventName: string,\n\tvalue: number?,\n\tcustomFields: CustomFields?\n): (boolean, string)\n```\n\nThe value defaults to `1`.\n\n### Economy events\n\n```luau\nAnalytics:LogEconomyEvent(\n\tplayer,\n\tEnum.AnalyticsEconomyFlowType.Sink,\n\t\"Coins\",\n\t250,\n\t750,\n\tEnum.AnalyticsEconomyTransactionType.Shop,\n\t\"DoubleJumpUpgrade\",\n\tAnalyticsKit.CreateCustomFields(\"MainShop\"),\n\tfalse\n)\n```\n\n```luau\nLogEconomyEvent(\n\tplayer: Player,\n\tflowType: Enum.AnalyticsEconomyFlowType,\n\tcurrencyType: string,\n\tamount: number,\n\tendingBalance: number,\n\ttransactionType: string | EnumItem?,\n\titemSku: string,\n\tcustomFields: CustomFields?,\n\tbatch: boolean?\n): (boolean, string)\n```\n\n`amount` must always be positive. The flow type determines whether Roblox treats it as a source or sink.\n\n### Onboarding funnel steps\n\n```luau\nAnalytics:LogOnboardingFunnelStepEvent(\n\tplayer,\n\t2,\n\t\"Choose Class\",\n\tAnalyticsKit.CreateCustomFields(\"Mage\")\n)\n```\n\n```luau\nLogOnboardingFunnelStepEvent(\n\tplayer: Player,\n\tstep: number,\n\tstepName: string,\n\tcustomFields: CustomFields?,\n\tallowDuplicate: boolean?\n): (boolean, string)\n```\n\n### Recurring funnel steps\n\n```luau\nlocal HttpService = game:GetService(\"HttpService\")\nlocal sessionId = HttpService:GenerateGUID(false)\n\nAnalytics:LogFunnelStepEvent(\n\tplayer,\n\t\"ShopCheckout\",\n\tsessionId,\n\t1,\n\t\"Opened Shop\"\n)\n```\n\n```luau\nLogFunnelStepEvent(\n\tplayer: Player,\n\tfunnelName: string,\n\tfunnelSessionId: string,\n\tstep: number,\n\tstepName: string,\n\tcustomFields: CustomFields?,\n\tallowDuplicate: boolean?\n): (boolean, string)\n```\n\n### Progression events\n\n```luau\nAnalytics:LogProgressionStartEvent(player, \"WorldAreas\", 3, \"Desert\")\nAnalytics:LogProgressionCompleteEvent(player, \"WorldAreas\", 3, \"Desert\")\nAnalytics:LogProgressionFailEvent(player, \"WorldAreas\", 3, \"Desert\")\n```\n\nThe general method is also available:\n\n```luau\nAnalytics:LogProgressionEvent(\n\tplayer,\n\t\"WorldAreas\",\n\tEnum.AnalyticsProgressionType.Complete,\n\t3,\n\t\"Desert\"\n)\n```\n\n## Registered events\n\nRegistered events provide a centralized, project-specific event catalog without placing game logic inside AnalyticsKit.\n\n```luau\nlocal Catalog = {\n\tBoostUpgraded = {\n\t\tKind = \"Custom\",\n\t\tEventName = \"Boost_Upgraded\",\n\t\tValue = 1,\n\t\tCustomFields = function(_player, data)\n\t\t\treturn AnalyticsKit.CreateCustomFields(\n\t\t\t\tdata.BoostType,\n\t\t\t\tdata.NewLevel\n\t\t\t)\n\t\tend,\n\t},\n\n\tCurrencyEarned = {\n\t\tKind = \"Economy\",\n\t\tFlowType = Enum.AnalyticsEconomyFlowType.Source,\n\t\tCurrencyType = function(_player, data)\n\t\t\treturn data.CurrencyType\n\t\tend,\n\t\tAmount = function(_player, data)\n\t\t\treturn data.Amount\n\t\tend,\n\t\tEndingBalance = function(_player, data)\n\t\t\treturn data.NewBalance\n\t\tend,\n\t\tItemSku = function(_player, data)\n\t\t\treturn data.ItemSku\n\t\tend,\n\t},\n}\n\nassert(Analytics:RegisterEvents(Catalog))\n```\n\nGameplay code can then use one stable entry point:\n\n```luau\nAnalytics:LogEvent(player, \"BoostUpgraded\", {\n\tBoostType = \"WalkSpeed\",\n\tNewLevel = 4,\n})\n```\n\nDefinition fields can be fixed values or resolver functions:\n\n```luau\nfunction(player: Player, data: any): any\n```\n\nA definition may also include:\n\n```luau\nEnabled = true\n\nValidate = function(player, data)\n\tif data.ItemSku == nil then\n\t\treturn false, \"ItemSku is required\"\n\tend\n\treturn true\nend\n```\n\n### Supported definition kinds\n\n- `Custom`\n- `Economy`\n- `Onboarding`\n- `Funnel`\n- `Progression`\n\nSee `AnalyticsCatalog.example.luau` for a compact example. `MigratedCatalog.example.luau` mirrors the project-specific event catalog from the earlier wrapper.\n\n## Studio behavior\n\nRoblox analytics events are not delivered from Studio, so AnalyticsKit provides explicit development behavior.\n\n### Print\n\n```luau\nStudioBehavior = \"Print\"\n```\n\nPrints normalized event summaries to the output. This is the default.\n\n### Record\n\n```luau\nStudioBehavior = \"Record\"\n```\n\nStores normalized events in memory:\n\n```luau\nlocal events = Analytics:GetRecordedEvents()\nlocal eventsAndClear = Analytics:GetRecordedEvents(true)\nAnalytics:ClearRecordedEvents()\n```\n\nThe retained count is controlled by `MaxRecordedEvents`.\n\n### Ignore\n\n```luau\nStudioBehavior = \"Ignore\"\n```\n\nAccepts valid calls but performs no Studio output or recording.\n\n## Custom transport and testing\n\nPass a transport callback to inspect normalized events without calling Roblox's AnalyticsService:\n\n```luau\nlocal captured = {}\n\nlocal Analytics = AnalyticsKit.new({\n\tTransport = function(record)\n\t\ttable.insert(captured, record)\n\tend,\n\tRateLimit = {\n\t\tEnabled = false,\n\t},\n})\n```\n\nA custom transport is used in both Studio and live servers. Transport errors are caught and emitted through `EventFailed`.\n\nNormalized records contain:\n\n```luau\n{\n\tKind = \"Economy\",\n\tPlayer = player,\n\tTimestamp = os.time(),\n\tClock = os.clock(),\n\tSequence = 12,\n\tRegisteredName = \"CurrencyEarned\",\n\tPayload = {\n\t\tFlowType = Enum.AnalyticsEconomyFlowType.Source,\n\t\tCurrencyType = \"Coins\",\n\t\tAmount = 10,\n\t\tEndingBalance = 250,\n\t\tTransactionType = \"Gameplay\",\n\t\tItemSku = \"PassiveIncome\",\n\t\tCustomFields = nil,\n\t},\n\tBatchCount = 10,\n}\n```\n\n## Signals\n\nAnalyticsKit exposes native `RBXScriptSignal` values backed by internal `BindableEvent` instances.\n\n```luau\nAnalytics.EventQueued:Connect(function(record, currentBatchCount)\n\tprint(\"Queued\", record.Payload.ItemSku, currentBatchCount)\nend)\n\nAnalytics.EventLogged:Connect(function(record)\n\tprint(\"Logged\", record.Kind)\nend)\n\nAnalytics.EventFailed:Connect(function(record, errorMessage)\n\twarn(record.Kind, errorMessage)\nend)\n```\n\nAvailable signals:\n\n| Signal | Arguments | Meaning |\n| --- | --- | --- |\n| `EventQueued` | `record, currentBatchCount` | An economy event was added to a batch |\n| `EventLogged` | `record` | The Roblox or custom transport completed successfully |\n| `EventRecorded` | `record, studioBehavior` | Studio printed or recorded an event |\n| `EventIgnored` | `record, reason` | A valid event was intentionally ignored |\n| `EventFailed` | `record, errorMessage` | The transport raised an error |\n| `EventDropped` | `record, reason` | Rate protection dropped a normalized event |\n| `BatchFlushed` | `summary` | One player's economy batches were flushed |\n\n## Validation\n\nAnalyticsKit validates common mistakes before they reach AnalyticsService:\n\n- Invalid or missing player\n- Empty event, SKU, currency, funnel, or progression names\n- Non-finite numbers\n- Economy amounts less than or equal to zero\n- Negative economy balances\n- Invalid enum types\n- Invalid funnel or progression levels\n- Unsupported custom-field keys or values\n- More than three custom fields\n\nDefault behavior warns and returns `false`:\n\n```luau\nlocal success, result = Analytics:LogCustomEvent(player, \"\", 1)\n```\n\nStrict mode raises an error instead:\n\n```luau\nlocal Analytics = AnalyticsKit.new({\n\tStrictValidation = true,\n})\n```\n\nAll logging methods return:\n\n```luau\nsuccess: boolean, statusOrError: string\n```\n\nCommon successful statuses are:\n\n- `Logged`\n- `Queued`\n- `Recorded`\n- `Ignored`\n- `Duplicate`\n\n## Funnel deduplication\n\nWith `DeduplicateFunnelSteps = true`, repeated steps are ignored within the current server session.\n\nOnboarding deduplication uses:\n\n```text\nplayer + onboarding step\n```\n\nRecurring funnel deduplication uses:\n\n```text\nplayer + funnel name + funnel session ID + step\n```\n\nThe session ID keeps separate runs of the same recurring funnel independent.\n\nPass `allowDuplicate = true` to a direct funnel call, or set `AllowDuplicate` in a registered definition, when repeated calls are intentional.\n\nReset cached steps with:\n\n```luau\nAnalytics:ResetFunnelDedupe(player)\nAnalytics:ResetFunnelDedupe()\n```\n\n## Rate-budget diagnostics\n\nRoblox documents a global AnalyticsService request limit based on concurrent users. AnalyticsKit estimates the current minute's usage from successful transport calls.\n\n```luau\nlocal requests = Analytics:GetRequestsInLastMinute()\nlocal estimatedLimit = Analytics:GetEstimatedRateLimit()\nlocal usage = Analytics:GetRateLimitUsage()\n```\n\nConfigure warning and overflow behavior:\n\n```luau\nRateLimit = {\n\tEnabled = true,\n\tWarningThreshold = 0.8,\n\tOverflowBehavior = \"Warn\",\n}\n```\n\n`Warn` reports the estimate but still attempts the event. `Drop` prevents calls after the estimated budget has been exhausted and fires `EventDropped`.\n\nThe estimate is local to the AnalyticsKit instance. Calls made directly to `AnalyticsService` elsewhere are not visible to it, so centralizing calls remains important.\n\n## Diagnostics\n\n```luau\nlocal diagnostics = Analytics:GetDiagnostics()\n\nprint(diagnostics.Attempted)\nprint(diagnostics.Logged)\nprint(diagnostics.Queued)\nprint(diagnostics.Failed)\nprint(diagnostics.PendingEconomyBatches)\nprint(diagnostics.RateLimitUsage)\n```\n\nAvailable values:\n\n```luau\n{\n\tAttempted: number,\n\tLogged: number,\n\tQueued: number,\n\tRecorded: number,\n\tIgnored: number,\n\tFailed: number,\n\tDropped: number,\n\tBatchesFlushed: number,\n\tEventsFlushed: number,\n\tRateWarnings: number,\n\tRequestsLastMinute: number,\n\tEstimatedRateLimit: number,\n\tRateLimitUsage: number,\n\tPendingEconomyBatches: number,\n\tRecordedEventCount: number,\n}\n```\n\nReset counters with:\n\n```luau\nAnalytics:ResetDiagnostics()\n```\n\n## Runtime controls\n\n```luau\nAnalytics:SetEnabled(false)\n\nAnalytics:SetEconomyBatchingEnabled(false)\nAnalytics:SetEconomyBatchingEnabled(false, false) -- Do not flush first\nAnalytics:SetEconomyBatchInterval(10)\n\nAnalytics:SetEconomySkuBatching(\"PassiveIncome\", true)\nAnalytics:SetEconomySkuBatching(\"PassiveIncome\", nil) -- Return to default policy\n\nAnalytics:SetEconomyTransactionType(\n\t\"CoinPack\",\n\tEnum.AnalyticsEconomyTransactionType.IAP\n)\n```\n\nManual flushing:\n\n```luau\nAnalytics:FlushEconomyBatchesForPlayer(player)\nAnalytics:FlushEconomyBatches()\n```\n\n## Player segments\n\nAnalyticsKit includes a protected wrapper for Roblox's yielding segment lookup:\n\n```luau\nlocal segments, errorMessage = Analytics:GetPlayerSegmentsAsync(player)\nif segments then\n\tprint(segments.ActivePayerStatus)\nelse\n\twarn(errorMessage)\nend\n```\n\nThe method returns an error in Studio.\n\n## Configuration\n\n```luau\nlocal Analytics = Anal","readmeTruncated":true,"readmeFull":"https://api.forest.dev/v1/package/biotoxin495/roblox/analyticskit/1.0.2/readme"}