2 Commits
0.4.0 ... 0.6.0

Author SHA1 Message Date
bdot
b898a75fde Publish Score iOS SDK 0.6.0 2026-10-07 20:38:39 -04:00
bdot
84b3a4cb20 Publish Score iOS SDK 0.5.0 2026-10-07 12:59:18 -04:00
6 changed files with 96 additions and 15 deletions

View File

@@ -8,9 +8,9 @@ identically — see [`conformance/CONTRACT.md`](conformance/CONTRACT.md).
--- ---
## Install — 0.4.0 beta (public, no auth) ## Install — 0.6.0 beta (public, no auth)
Distributed publicly from **[`3HL-SCORE/public-sdks-score`](https://git.3halves-labs.com/3HL-SCORE/public-sdks-score)** (#284) — **no Gitea token / login required**. The iOS XCFramework is a public **release asset** (`mobile-v0.4.0`) on that repo; the Android artifacts live in the org's **Gitea Maven package registry** (anonymous read). Distributed publicly from **[`3HL-SCORE/public-sdks-score`](https://git.3halves-labs.com/3HL-SCORE/public-sdks-score)** (#284) — **no Gitea token / login required**. The iOS XCFramework is a public **release asset** (`mobile-v0.6.0`) on that repo; the Android artifacts live in the org's **Gitea Maven package registry** (anonymous read).
### Android — Gradle (public Maven registry) ### Android — Gradle (public Maven registry)
```kotlin ```kotlin
@@ -18,7 +18,7 @@ repositories {
maven { url = uri("https://git.3halves-labs.com/api/packages/3HL-SCORE/maven") } // public — no credentials maven { url = uri("https://git.3halves-labs.com/api/packages/3HL-SCORE/maven") } // public — no credentials
} }
dependencies { dependencies {
implementation("com.threehalveslabs.scoretracker:shared-android:0.4.0") implementation("com.threehalveslabs.scoretracker:shared-android:0.6.0")
} }
``` ```
@@ -34,17 +34,17 @@ add it by **git URL** — no registry config, no login:
```swift ```swift
// your app's Package.swift // your app's Package.swift
dependencies: [ dependencies: [
.package(url: "https://git.3halves-labs.com/3HL-SCORE/public-sdks-score.git", from: "0.4.0") .package(url: "https://git.3halves-labs.com/3HL-SCORE/public-sdks-score.git", from: "0.6.0")
], ],
// target: .product(name: "ScoreTracker", package: "public-sdks-score") // target: .product(name: "ScoreTracker", package: "public-sdks-score")
``` ```
Or in Xcode: **File → Add Packages…** → paste the URL. The binary `ScoreTrackerKit` Or in Xcode: **File → Add Packages…** → paste the URL. The binary `ScoreTrackerKit`
target pulls the XCFramework from the **public** `mobile-v0.4.0` release asset (SPM target pulls the XCFramework from the **public** `mobile-v0.6.0` release asset (SPM
verifies the checksum) — no `~/.netrc` credentials needed. Verified with an verifies the checksum) — no `~/.netrc` credentials needed. Verified with an
anonymous `swift package resolve`. anonymous `swift package resolve`.
### iOS — direct XCFramework ### iOS — direct XCFramework
Download `ScoreTracker-pod.zip` from the **mobile-v0.4.0** release on Download `ScoreTracker-pod.zip` from the **mobile-v0.6.0** release on
[`public-sdks-score`](https://git.3halves-labs.com/3HL-SCORE/public-sdks-score/releases). [`public-sdks-score`](https://git.3halves-labs.com/3HL-SCORE/public-sdks-score/releases).
It holds `ScoreTrackerKit.xcframework` and the Swift facade sources It holds `ScoreTrackerKit.xcframework` and the Swift facade sources
(`Sources/ScoreTracker/`: `ScoreClient.swift`, `Exports.swift`, `AppLifecycle.swift`). (`Sources/ScoreTracker/`: `ScoreClient.swift`, `Exports.swift`, `AppLifecycle.swift`).
@@ -155,6 +155,39 @@ event, pass it yourself: `requires: [.advertising]` on iOS (or the
`trackAdvertising(...)` shorthand), `requires = listOf(ConsentCategory.ADVERTISING)` `trackAdvertising(...)` shorthand), `requires = listOf(ConsentCategory.ADVERTISING)`
on Android. on Android.
## Regional privacy policy (GDPR, Quebec, LGPD, CPRA)
One `privacy` option sets the tracker's posture for the fan's region (#489),
with the same policies and values as the web tracker's `privacy` option. For URC,
which is UK/EU-facing, use **GDPR**:
```swift
// iOS
let score = ScoreClient(publishableKey: pk, siteId: "urc-ios", endpoint: endpoint,
privacy: PrivacyOptions(mode: .gdpr, region: nil, regionProvider: nil, overrides: nil))
```
```kotlin
// Android
val score = newScoreTracker(context, TrackerConfig(pk, "urc-android", endpoint,
privacy = PrivacyOptions(PrivacyMode.GDPR)))
```
| Policy | Starting consent | Queue kept at most | Drops `locale` / `device` |
|---|---|---|---|
| `GDPR` (EU, EEA, UK, CH) | nothing granted (opt-in) | 24 h | yes |
| `QUEBEC` (Law 25) | nothing granted | 24 h | yes |
| `LGPD` (Brazil) | nothing granted | 48 h | yes |
| `CPRA` (US) | analytics + personalization; not advertising | 168 h (the tracker's 72 h still applies) | no |
| `OFF` | no policy: as without the option | 72 h | no |
What the policy changes:
- **Starting consent:** applies only when you pass no `initialConsent`, and only until your first `setConsent`. A value you pass from your CMP always wins. Under GDPR nothing is sent until `setConsent`, as before.
- **Retention:** queued events older than the cap are dropped instead of sent, including events saved before a cold start.
- **Fingerprint-adjacent signals:** `locale` and the device model leave the envelope. `os` and `appVersion` stay.
- **Declared only:** "no session replay" and "no geo enrichment" are reported by `inspect()`, but the SDK does neither anyway.
`AUTO` picks the policy from `region` (ISO country, optionally with a subdivision: `GB`, `US-CA`, `CA-QC`) or a `regionProvider`; with neither, it uses GDPR, the strictest. The device locale is never used: it's a language setting, not a location. Resolve the region before creating the tracker; the policy is fixed for its lifetime. To tune a setting without forking the table, pass `overrides`, e.g. `PrivacyOverrides(retentionMaxHours = 12)`. `inspectJson()` shows the active policy under `privacy`.
## PII scrubbing ## PII scrubbing
Same options as the web tracker's `pii` (#482). Scrubbing runs **when you call Same options as the web tracker's `pii` (#482). Scrubbing runs **when you call
@@ -241,6 +274,15 @@ Events still waiting in the queue aren't replayed: they go out with the fan on
the next flush. The buffer lives in memory only, so it's empty after a cold the next flush. The buffer lives in memory only, so it's empty after a cold
start. start.
After `identify`, the tracker also links this device's `anonymousId` to the fan
(`POST /api/identity/link-anonymous`, with the token from your provider or
`setToken`), so the server can credit events that arrive with only the
`anonymousId`. It happens once per fan per device, on the next flush, after the
events; it needs `analytics` consent and a token, and a failure never affects
event delivery. Android's `score.lastLinkSkipReason()` (`already_linked`,
`consent`, `no_token`, `error`) says why a link hasn't happened. A custom
`Transport` must implement `postWithHeaders` for this call to carry the token.
How the provider is used: How the provider is used:
- **Before each flush**, and **single-flight**: overlapping flushes share one - **Before each flush**, and **single-flight**: overlapping flushes share one
in-flight call instead of starting several. in-flight call instead of starting several.
@@ -305,6 +347,28 @@ How the provider is used:
`startAutoFlush(everySeconds: 30)` (iOS) / `startAutoFlush(scope, intervalMs)` `startAutoFlush(everySeconds: 30)` (iOS) / `startAutoFlush(scope, intervalMs)`
(Android). (Android).
## Bug reports: attach `inspectJson()`
When the tracker misbehaves in the field, the first thing to attach is its
snapshot (#490), the counterpart of the web tracker's `inspect()`:
```swift
print(score.inspectJson()) // iOS
```
```kotlin
Log.d("Score", score.inspectJson()) // Android
```
It covers:
- the SDK name and version
- consent: each category, and how many events the gate dropped
- the queue: depth, caps, the oldest event's age, critical events, critical events lost
- identity: `anonymousId`, `sessionId`, `userId`, whether a token is held, the `link-anonymous` state and the replay buffer
- the last flush: when, whether it succeeded, how many events it sent, and how many failures in a row
- site, endpoint and the number of PII rules
**It never contains the token**, only `hasToken`, so it's safe to log or paste. `inspect()` returns the same data as a typed `InspectSnapshot`.
## Canonical app earning events ## Canonical app earning events
`type: "track"` with these names (props open; matched by the Odoo rules engine): `type: "track"` with these names (props open; matched by the Odoo rules engine):

View File

@@ -15,8 +15,8 @@ let package = Package(
targets: [ targets: [
.binaryTarget( .binaryTarget(
name: "ScoreTrackerKit", name: "ScoreTrackerKit",
url: "https://git.3halves-labs.com/3HL-SCORE/public-sdks-score/releases/download/mobile-v0.4.0/ScoreTrackerKit.xcframework.zip", url: "https://git.3halves-labs.com/3HL-SCORE/public-sdks-score/releases/download/mobile-v0.6.0/ScoreTrackerKit.xcframework.zip",
checksum: "7a900bb0b672188f1bd994aa9161817f6318d2746a4c32c4a56ca95a27392ade" checksum: "dbf9bd9eec9e0c01cb28d7527e19e15909ddda95cf5fb48e50a1639e18ef1988"
), ),
.target( .target(
name: "ScoreTracker", name: "ScoreTracker",

View File

@@ -9,13 +9,13 @@ Public, auth-free distribution of the **Score mobile tracker** SDKs for iOS and
> This repo hosts the **consumable artifacts** (release assets + package manifests). The SDK source lives in 3 Halves Labs' private platform repo. > This repo hosts the **consumable artifacts** (release assets + package manifests). The SDK source lives in 3 Halves Labs' private platform repo.
## Install — 0.4.0 (no token / login required) ## Install — 0.6.0 (no token / login required)
### iOS — Swift Package Manager ### iOS — Swift Package Manager
Add this repo by git URL. There's no registry to configure and no login. Add this repo by git URL. There's no registry to configure and no login.
```swift ```swift
// Package.swift // Package.swift
dependencies: [ .package(url: "https://git.3halves-labs.com/3HL-SCORE/public-sdks-score.git", from: "0.4.0") ] dependencies: [ .package(url: "https://git.3halves-labs.com/3HL-SCORE/public-sdks-score.git", from: "0.6.0") ]
// target: .product(name: "ScoreTracker", package: "public-sdks-score") // target: .product(name: "ScoreTracker", package: "public-sdks-score")
``` ```
Or in Xcode: **File → Add Packages…** and paste the URL. (The old Swift package Or in Xcode: **File → Add Packages…** and paste the URL. (The old Swift package
@@ -29,7 +29,7 @@ pod 'ScoreTracker',
``` ```
### iOS — direct XCFramework ### iOS — direct XCFramework
Download `ScoreTracker-pod.zip` from the [`mobile-v0.4.0` release](https://git.3halves-labs.com/3HL-SCORE/public-sdks-score/releases). It contains `ScoreTrackerKit.xcframework` plus the Swift facade sources (`Sources/ScoreTracker/`). Drag the framework into Xcode and add the Swift files to your app target. Download `ScoreTracker-pod.zip` from the [`mobile-v0.6.0` release](https://git.3halves-labs.com/3HL-SCORE/public-sdks-score/releases). It contains `ScoreTrackerKit.xcframework` plus the Swift facade sources (`Sources/ScoreTracker/`). Drag the framework into Xcode and add the Swift files to your app target.
### Android — Gradle ### Android — Gradle
```kotlin ```kotlin
@@ -37,7 +37,7 @@ repositories {
maven { url = uri("https://git.3halves-labs.com/api/packages/3HL-SCORE/maven") } // public — no credentials maven { url = uri("https://git.3halves-labs.com/api/packages/3HL-SCORE/maven") } // public — no credentials
} }
dependencies { dependencies {
implementation("com.threehalveslabs.scoretracker:shared-android:0.4.0") implementation("com.threehalveslabs.scoretracker:shared-android:0.6.0")
} }
``` ```

View File

@@ -1,6 +1,6 @@
Pod::Spec.new do |s| Pod::Spec.new do |s|
s.name = 'ScoreTracker' s.name = 'ScoreTracker'
s.version = '0.4.0' s.version = '0.6.0'
s.summary = 'Score mobile tracker (iOS) — KMP shared core + Swift facade.' s.summary = 'Score mobile tracker (iOS) — KMP shared core + Swift facade.'
s.description = <<-DESC s.description = <<-DESC
Lightweight app analytics / fan-engagement tracker. Emits the same event Lightweight app analytics / fan-engagement tracker. Emits the same event

View File

@@ -17,3 +17,11 @@ public typealias DeliveryMode = ScoreTrackerKit.DeliveryMode
public typealias PiiOptions = ScoreTrackerKit.PiiOptions public typealias PiiOptions = ScoreTrackerKit.PiiOptions
public typealias PiiRule = ScoreTrackerKit.PiiRule public typealias PiiRule = ScoreTrackerKit.PiiRule
public typealias PiiAction = ScoreTrackerKit.PiiAction public typealias PiiAction = ScoreTrackerKit.PiiAction
/// The support snapshot returned by `ScoreClient.inspect()` (#490); its nested
/// types are reached through it (`snapshot.identity.hasToken`).
public typealias InspectSnapshot = ScoreTrackerKit.InspectSnapshot
/// Regional privacy policy (#489): pass `PrivacyOptions(mode: .gdpr, …)` as
/// `ScoreClient(privacy:)`.
public typealias PrivacyOptions = ScoreTrackerKit.PrivacyOptions
public typealias PrivacyMode = ScoreTrackerKit.PrivacyMode
public typealias PrivacyOverrides = ScoreTrackerKit.PrivacyOverrides

View File

@@ -28,7 +28,8 @@ public final class ScoreClient {
os: String? = nil, os: String? = nil,
device: String? = nil, device: String? = nil,
locale: String? = nil, locale: String? = nil,
initialConsent: ConsentState = ConsentState.companion.NONE, initialConsent: ConsentState? = nil,
privacy: PrivacyOptions? = nil,
pii: PiiOptions? = nil, pii: PiiOptions? = nil,
tokenRefreshTimeoutSeconds: Double = 5, tokenRefreshTimeoutSeconds: Double = 5,
identifyReplayBufferSize: Int = 100 identifyReplayBufferSize: Int = 100
@@ -37,7 +38,8 @@ public final class ScoreClient {
publishableKey: publishableKey, publishableKey: publishableKey,
siteId: siteId, siteId: siteId,
endpoint: endpoint, endpoint: endpoint,
device: DeviceDescriptor(os: os, device: device, appVersion: appVersion, locale: locale) device: DeviceDescriptor(os: os, device: device, appVersion: appVersion, locale: locale),
privacy: privacy
) )
tracker = IosPlatformKt.doNewScoreTracker( tracker = IosPlatformKt.doNewScoreTracker(
config: config, config: config,
@@ -120,6 +122,13 @@ public final class ScoreClient {
public var anonymousId: String { tracker.anonymousId } public var anonymousId: String { tracker.anonymousId }
public var pending: Int { Int(tracker.pending()) } public var pending: Int { Int(tracker.pending()) }
/// The tracker's state for support (#490): consent, queue depth and oldest
/// event, identity (the token only as `hasToken`), the last flush, and the
/// link and replay state. Attach `inspectJson()` to any bug report.
public func inspect() -> InspectSnapshot { tracker.inspect() }
/// `inspect()` as JSON, ready to paste into a bug report.
public func inspectJson() -> String { tracker.inspectJson() }
// MARK: - Tracking // MARK: - Tracking
/// Track a typed loyalty event — the preferred form (#485). `Events` is /// Track a typed loyalty event — the preferred form (#485). `Events` is