hotfy_sdk 2.2.0
hotfy_sdk: ^2.2.0 copied to clipboard
SDK da Hotfy para Flutter — wrapper (orquestração de ads AdMob/GAM), CDP (eventos, atribuição, ad revenue/ILAR, push) e device token (sync FCM/APNS no Console + CDP).
Changelog #
Todas as mudanças notáveis deste pacote são documentadas aqui.
O formato segue Keep a Changelog e o versionamento segue Semantic Versioning.
2.2.0 #
Added #
collectAdvertisingIdnoAnalyticsConfig(defaulttrue). Optar por não coletar o GAID passa a ser configuração explícita, em vez de efeito colateral de a origem do dado estar indisponível. Paridade com o SDK React Native.adTrackingLimitedno contexto do evento (ad_tracking_limitedno JSON), para que o servidor consiga separaradvertising_idvazio por escolha do usuário de vazio por falha. Sem esse sinal os dois são indistinguíveis e a saúde do campo não é monitorável.
Fixed #
- O nativo colapsava três desfechos em
success(null).HotfySdkPlugin.getAdvertisingIddevolvianulltanto quando o usuário limitava o rastreamento (comportamento correto, a respeitar) quanto quando o Play Services falhava (bug). Agora devolve{ id, isAdTrackingLimited }e reporta exceção porresult.error, não porsuccess(null). - Erro na leitura do advertising ID passa a avisar, fora do debug-gate — mesma razão do descarte por fila cheia (invariante I2 do Contrato): degradação de dado nunca acontece em silêncio.
Changed #
- A checagem de plataforma usa
defaultTargetPlatformno lugar dePlatform.isAndroiddodart:io. Em teste odart:ioreporta o host, o que tornava esse caminho inexercitável — e caminho não exercitado foi exatamente como o gap equivalente do RN passou meses sem ninguém ver. Em release o override de debug não existe, então o valor é sempre a plataforma real.
Contrato #
Novo test/cdp_device_context_test.dart implementando §9.4, com os mesmos nomes do
context.test.ts do RN: context_advertising_id_present_when_available,
context_limit_ad_tracking_is_empty_and_silent,
context_collect_advertising_id_false_skips_collection,
context_missing_advertising_source_warns_once.
2.1.3 #
Fixed #
- Remoção do lote virou seguro por padrão (contrato §4.2). O lote era retirado da
fila com
removeRangeANTES do envio, então cada ramo doflush()precisava lembrar de devolvê-lo em caso de falha. O default de qualquer caminho novo era PERDER — e foi exatamente assim que o bug do 2.1.2 nasceu: alguém adicionou umcatche esqueceu. Agora o lote é lido comtakee só sai da fila quando confirmado que saiu (entregue ou descartado). Perder passa a exigir ação explícita. - Remoção por
eventId, nunca por posição. O descarte por estouro (D7) remove do início da fila; se ocorrer durante um envio, os índices deslocam e umremoveRange(0, N)removeria eventos que não são do lote — perda silenciosa por outro caminho.
2.1.2 #
Fixed #
- Perda silenciosa de eventos quando o envio lança (contrato §5 D6). O
flush()retirava o lote da fila antes de enviar; se opostlançasse — rede, DNS, timeout de 10s, cliente parado — ocatchapenas logava, e só em debug. O lote não voltava e o espelho não era atualizado; o próximoenqueuegravava a fila truncada por cima, tornando a perda definitiva. Como olifecycleforça flush ao ir para background, isso acontecia justamente quando o usuário sai do app depois do anúncio. A cobertura medida caiu de ~91% para ~86% em produção por causa disso. 400e demais 4xx permanentes passam a descartar o lote (D2). Antes caíam no ramo genérico e eram re-enfileirados: um400girava em laço apertado e, por ficar na frente da fila, prendia todos os eventos atrás dele.- Regra anti-laço na continuação do dreno (§4.1). A recursão do fim do
flush()só continua se o lote saiu. Esse laço já existia no caminho de5xx; não aparecia porque o lote era perdido e a fila encolhia sozinha — um bug mascarava o outro. shutdown()parava o transporte antes do flush final (§7.3), destruindo tudo que estivesse pendente. Ordem invertida.- Estouro de
maxQueueSizeagora avisa ao descartar (I2). Antes sumia sem log. sdkVersiondos eventos estava congelado em1.0.0. Existiam duas constantes homônimas: a gerada dopubspec.yaml(usada porHotfy.version, correta) e um literal emcdp/config.dart, que era o carimbado em todo evento. O console de debug mostrava a versão certa enquanto os dados diziam1.0.0.
Added #
- Suíte do Contrato de Entrega de Eventos (
test/cdp_event_delivery_contract_test.dart), com nomes de caso idênticos aos do SDK React Native. Se um SDK for corrigido e o outro não, falta um teste de nome conhecido — em vez de a divergência ficar semanas escondida.
2.1.1 #
Fixed #
-
winningFloorTtlDaysagora vale pra TODOS os formatos. Só ointerstitialpassava ottlDaysproCascadeRunner—rewarded,banner(×2) eboot/app-open (×2) nunca liam a config. ComogetWinningFloortratattlDaysnull como "nunca expira", o winning floor cacheado desses formatos ficava cravado pra sempre no último floor que vendeu, e o TTL configurado no console não tinha efeito nenhum. Na prática o TTL era feature exclusiva do interstitial desde que foi criado — 5 dos 7 call sites ignoravam. O mesmo bug existia no SDK React Native (corrigido lá na 2.1.1/2.1.2). -
showRewardedagora usa orewarded_fallback. O rewarded era o único formato que não caía pro unit de fallback quando a cascata do primary esgotava —interstitial,banner,nativeeapp_openjá usavam o seu. O slot é configurável no console e era silenciosamente ignorado. Agora espelha oshowInterstitial: cascata do primary → se tudo der no-fill, cascata do fallback → senãoskip(slot_not_ready). Só no-fill desce pro fallback; erro não-de-demanda (network/internal) continua abortando, e um ad já mostrado não dispara outro. O eventoshowdo rewarded agora emiteisFallback. -
O log do
customTargetingagora mostra a key real. Ele hardcodavaprice_floor=no texto enquanto a key enviada vinha dopriceFloorTargetingKeyda config (defaulthotfy_price_floor). Quando um app configura key própria, o log seguia dizendoprice_floor=e não havia como saber o que foi enviado — e key que não casa com a UPR do GAM dá no-fill com exatamente o mesmo sintoma de floor errado ou rule ausente. Os três casos eram indistinguíveis em campo. Mesmo bug do SDK RN.
Changed #
CascadeRunnerInit.ttlDaysvirou obrigatório (aceitandonull). Comonullsignifica "nunca expira", um call site que esquecesse o campo desligava o TTL sem erro do analyzer, sem warning e sem teste pegando — foi exatamente assim que os 5 call sites acima quebraram, em Flutter e RN, sem ninguém notar por versões. Agora esquecer é erro de compilação. Mudança interna:CascadeRunnernão é API pública.
2.1.0 #
Added #
- Atribuição para o CDP agora é automática. No
Hotfy.init, os sinais que o wrapper captura do Install Referrer (Android) / deep link (iOS) —gclid,gbraid/wbraid, dados do Meta e UTMs — passam a ser repassados sozinhos proPOST /v1/attribution, sem o app precisar chamarcaptureAttribution(...). Antes, esquecer essa chamada (ou chamá-la sem os sinais) fazia o CDP classificar os installs comosource="unknown". O repasse:- só roda com
adsEnabled(sem wrapper não há o que repassar); - só dispara quando há sinal de origem real — instalação orgânica sem referrer
não gera linha
unknown; - é idempotente (envia 1x por install), então coexiste com um
captureAttribution(...)manual sem duplicar envio.
- só roda com
AttributionParamsganhou os camposgbraid,wbraidemetaCampaignGroupId(mapeados pro corpo do/v1/attribution), pra não perder sinais no repasse.
Notas #
Hotfy.captureAttribution(...)continua público como escape hatch — apps não precisam mais chamá-lo, mas quem chama segue funcionando (a guarda de idempotência evita envio duplicado).
2.0.1 #
Fixed #
- Analytics: fim do retry storm em falha de auth. Uma key inválida/expirada
fazia o flush de eventos (
POST /v1/batch) retornar401/403e oEventQueuere-enfileirava o batch e re-chamavaflush()imediatamente, num loop apertado (~1 req/s por device) que nunca convergia — com potencial de sobrecarregar o backend em escala. Agora401/403são tratados como falha permanente da key atual: o flush pausa (os eventos continuam na fila e persistidos) até um novoHotfy.initcom uma key válida. Erros transitórios (5xx/429) seguem com o backoff habitual.
2.0.0 #
⚠️ Breaking — key única + nomes genéricos (unificação CDP ↔ Console) #
O SDK passa a usar uma única API key (a App.apiKey do Hotfy Console) pros três
módulos, e os nomes públicos com jargão interno (wrapper/cdp) viraram genéricos
(ads/analytics).
Config do init() — apiKey no topo; cdp/wrapper viram ads/analytics (overrides opcionais):
// antes
await Hotfy.init(SdkConfig(
cdp: CdpConfig(apiKey: cdpKey),
wrapper: WrapperInitConfig(apiKey: consoleKey),
));
// agora
await Hotfy.init(SdkConfig(apiKey: consoleKey));
O formato antigo continua acessível via SdkConfig.legacy(cdp:, wrapper:) (compat) — migre quando puder.
Tipos renomeados (sem alias — atualize os imports):
| Antes | Agora |
|---|---|
CdpConfig |
AnalyticsConfig |
CdpEvent |
AnalyticsEvent |
WrapperInitConfig |
AdsConfig |
WrapperConfig |
AdsResolvedConfig |
WrapperBanner / WrapperBannerSize |
AdBanner / AdBannerSize |
WrapperNativeAd |
AdNativeAd |
WrapperAdEvent / WrapperEventName / WrapperEventListener |
AdEvent / AdEventName / AdEventListener |
WrapperAdUnits |
ResolvedAdUnits |
WrapperRoute |
AdsRoute |
Changed #
- Tags de log padronizadas:
[Hotfy:cdp]→[Hotfy:analytics],[Hotfy:wrapper]→[Hotfy:ads]. - README reescrito pro formato de key única (push token vira opt-in via
deviceToken: DeviceTokenConfig()).
1.1.4 #
Nota: a 1.1.3 foi pulada — a tag foi criada mas o publish falhou (drift de versão); estas mudanças saem na 1.1.4.
Added #
- Lock de coordenação de fullscreen. Garante que dois ads fullscreen
(interstitial / app open / rewarded) nunca apareçam ao mesmo tempo nem em
sequência imediata.
canShowFullscreen()gateia todos osshow(); o flag de "ad na tela" passa a contar só formatos fullscreen (banner/native não travam) e ganha janela anti-empilhamento de 1.5s pós-close. Resolve o "ad surgindo do nada" quando o app open de foreground colide com o gate de ad de uma tela (ex: chat). Novo motivo de skip:ad_already_showing. - Pré-carga do App Open de foreground (warm return). Na ida pro background o SDK pré-carrega o app open — escalando primary → fallback (esquenta o unit que realmente enche) — e, no retorno (≥30s, fora de cooldown), mostra instantâneo, sem a latência do load on-demand. Guard de frescor de 3.5h + gate de cooldown; fallback transparente pro on-demand quando não há warm pronto.
- Open-tracking de push no SDK.
Hotfy.trackConsolePushOpened(campaignId)reporta o open ao console (/v1/push-events, api_key-only — sem app_id; resolve o anonymousId do CDP automaticamente).Hotfy.parsePushData(data)extraicampaignId/targetScreen/targetParams/deeplink/isTestdo payload do push. Substitui o fetch hand-rolled no handler de notificação; CDP segue com o tracking próprio (Hotfy.trackPushOpened).
1.1.2 #
Fixed #
Hotfy.versionreportava a versão errada. A constsdkVersionemsrc/version.dartera mantida à mão e ficou em1.1.0na release1.1.1(drift). Agorasrc/version.darté gerado depubspec.yamlportool/gen_version.dart(equivalente Dart dogen-version.mjsdo SDK React Native) e um check no CI (PR Checks) falha se o arquivo gerado divergir do pubspec —Hotfy.versionnão sai mais de sync com a versão publicada.
1.1.1 #
Added #
Hotfy.version. Versão do SDK exposta em runtime (desrc/version.dart), útil pra exibir no debug console e confirmar qual build está rodando.- Override de segmento (debug/QA).
Hotfy.setSegmentOverride(slug),Hotfy.getSegmentOverride(),Hotfy.getSegment()e a constanteavailableSegments. Força qualquer Wrapper-Segment mandando um body limpo (source_type+days_since_install) no/v1/wrapper/config— sem mudança de backend. O slug persiste (SharedPreferences) e a troca refaz o fetch + reseta o ad pool. Passarnullvolta à resolução natural. - TTL do winning floor.
WrapperConfig.winningFloorTtlDays(por ad unit, vindo da colunawinning_floor_ttl_daysda pricing rule). Depois de N dias o cache do floor vencedor expira e a cascata reprova do topo. Janela conta desde que o floor virou vencedor (não reseta a cada venda). Ausente/0 = nunca expira (comportamento legado). Aplicado ao interstitial. - Custom Targeting Key dinâmica.
WrapperConfig.priceFloorTargetingKey— o SDK injeta essa key nocustomTargetingem vez da fixa, com fallbackhotfy_price_floor. Permite cada app usar a própria key sem rebuild.
Changed #
- Observabilidade da cascata nos logs. O log de erro do preload agora inclui
floor=(identifica qual variante deu no-fill), e o show emiteCASCADE_NEXTao descer de variante — deixando a cascata legível linha-a-linha. - Formato do winning floor no storage. Agora JSON
{floor, savedAt}(era a string crua do floor) pra carregar o timestamp do TTL. Back-compat: entries legadas são lidas normalmente.
1.0.7 #
Changed #
-
Price floor só vai na request quando configurado no console. Removido o fallback hardcoded (
1.86) que era aplicado a ad units GAM sem rule de price_floor. Na prática esse piso forçado não tinha UPR correspondente no GAM para units não configurados, garantindo No Fill. Agora, sem rule ativa, ocustomTargeting.hotfy_price_floornão é enviado — o GAM decide o fill sem piso imposto pelo cliente. Ad units com rule no console seguem inalterados.Internamente, units sem piso usam o sentinel
noFloorcomo chave de slot/ cascata (mantém preload e show consistentes); o log passa a mostrarprice_floor=none source=unconfigured. Afeta todos os formatos (interstitial, banner, rewarded, app open/boot).
1.0.6 #
Fixed #
-
hotfy_price_floornão chegava no GAM (todos os formatos). Os ad units GAM (/network/...) eram carregados pelas classes AdMob (InterstitialAd.load,AppOpenAd.load,RewardedAd.load,BannerAd), que convertem o request viaasAdRequest()na camada nativa e descartam ocustomTargeting. Resultado: o piso era montado e logado, mas nunca enviado no request — as UPRs do GAM não casavam e o resultado era No Fill (só backfill AdX a $0).Agora cada loader ramifica por rede (
detectNetwork): unit GAM usa o loader AdManager (AdManagerInterstitialAd.load,AppOpenAd.loadWithAdManagerAdRequest,RewardedAd.loadWithAdManagerAdRequest,AdManagerBannerAd), que preserva ocustomTargeting. Unit AdMob (ca-app-pub-...) segue pelo loader AdMob, sem targeting (não suportado).
1.0.5 #
Fixed #
- Logs de ad (
[Hotfy:ads:*]PRELOAD/SHOW/PAID/SKIP/CLOSE/LOAD viaadLog) não apareciam mesmo comdebug: true—setWrapperDebug()estava definido mas nunca era chamado, deixando_wrapperDebugsemprefalse. Agora o init do wrapper chamasetWrapperDebug(config.debug).
1.0.4 #
Changed #
- Prefixos de log alinhados 1:1 com o SDK React Native (
@hotfyllc/sdk):[HotfyCdp]→[Hotfy:cdp],[HotfyWrapper]→[Hotfy:wrapper]. Logs de ads passam deAd [INTERSTITIAL]para[Hotfy:ads:interstitial](sufixo:testquandouseTestAds).[Hotfy:device-token]já estava no padrão. Casa com o filtroaddo debug console ([hotfy:ads).
1.0.3 #
Alterado #
- Ajustes internos de documentação e comentários (sem mudança de API).
- CI:
actions/checkoutatualizado parav5(Node 24).
1.0.0 #
Primeira versão do hotfy_sdk — SDK da Hotfy para Flutter.
Adicionado #
- Módulo wrapper — orquestração remota de ads (interstitial, app open,
rewarded, banner, native) com AdMob/GAM, configurada via Hotfy App Console.
- Fetch de config via
POST /v1/wrapper/config(headerx-api-key). - Captura automática de atribuição: Install Referrer (Android) e deep link
inicial (iOS), com cálculo de
daysSinceInstall. - Price floors + cascade (pré-carrega instâncias por floor, avança em no-fill, cacheia o floor vencedor por ad unit).
- Sistema de eventos (
load,show,impression,click,close,error,skip) viaHotfy.on(...). isAdShowingglobal pra suprimir reações a background falso.
- Fetch de config via
- Módulo CDP (Customer Data Platform) — eventos custom, screen views,
identify, atribuição, ad revenue (ILAR), push events e offline queue persistida com retry. - Módulo device token — registro automático do FCM/APNS no Console E no CDP, com change-detection, retry e safety re-sync de 30 dias.
- Fachada
Hotfy— API flat única (Hotfy.init(...)e métodos dos três módulos).