Custom providers¶
A provider is anything that implements one or more of Herald's five interfaces. Herald's vendor modules build theirs from factory chains, but a provider can be much simpler.
Your own backend¶
To send events to your own collection endpoint, implement EventTrackerService:
struct BackendAnalytics: EventTrackerService {
let api: any EventsAPI
func track(_ event: any Event) {
var parameters: [String: String] = [:]
for (key, value) in event.parameters {
parameters[key] = value.asString
}
// A network request is slow, so it runs in a Task and `track` returns at once. A failure
// reported from that Task still reaches Herald's error reporter.
Task {
do {
try await api.send(name: event.name, parameters: parameters)
} catch {
Herald.reportFailure(error)
}
}
}
}
export class BackendAnalytics implements EventTrackerService {
constructor(private readonly api: EventsApi) {}
track(event: Event): Promise<void> {
const parameters: Record<string, string> = {};
for (const [key, value] of Object.entries(parametersOf(event))) {
parameters[key] = AnalyticsValue.asString(value);
}
return this.api.send(event.name, parameters);
}
}
Register it like any vendor, with only the interfaces it implements. It's called with the others, and its failures are caught and reported like theirs.
iOS
Herald calls each provider on your thread and waits for it to return, so a provider must return
quickly. Do slow work, such as this network request, in a Task, and report a failure there
with Herald.reportFailure. Herald still knows which provider and call it belongs to. Don't
report from a completion handler on another queue: Herald can't tell where that report came
from.
Wrapping a provider¶
EventTrackerService has a single method, so wrapping one takes a few lines. That's the place for
rules that depend on the situation rather than on the event: sampling, a runtime switch, skipping
repeated events, keeping personal data on the device.
/** Only a share of events, for a vendor that bills per event. */
fun EventTrackerService.sampled(rate: Double, random: Random = Random.Default) =
EventTrackerService { event -> if (random.nextDouble() < rate) track(event) }
/** Everything except what [excluded] matches. */
fun EventTrackerService.except(excluded: (Event) -> Boolean) =
EventTrackerService { event -> if (!excluded(event)) track(event) }
/// Sends only some events, for a vendor that charges per event.
struct SampledEventTracker: EventTrackerService {
let inner: any EventTrackerService
let rate: Double
func track(_ event: any Event) {
if Double.random(in: 0..<1) < rate {
inner.track(event)
}
}
}
struct ExceptEventTracker: EventTrackerService {
let inner: any EventTrackerService
let excluded: @Sendable (any Event) -> Bool
func track(_ event: any Event) {
if !excluded(event) {
inner.track(event)
}
}
}
extension EventTrackerService {
func sampled(_ rate: Double) -> any EventTrackerService {
SampledEventTracker(inner: self, rate: rate)
}
func except(_ excluded: @escaping @Sendable (any Event) -> Bool) -> any EventTrackerService {
ExceptEventTracker(inner: self, excluded: excluded)
}
}
/// Sends only some events, for a vendor that charges per event.
final class SampledEventTracker(
final EventTrackerService inner,
final double rate, [
Random? random,
]) implements EventTrackerService {
final Random _random = random ?? Random();
@override
Future<void> track(Event event) async {
if (_random.nextDouble() < rate) await inner.track(event);
}
}
final class const ExceptEventTracker(
final EventTrackerService inner,
final bool Function(Event event) excluded,
) implements EventTrackerService {
@override
Future<void> track(Event event) async {
if (!excluded(event)) await inner.track(event);
}
}
extension EventTrackerDecorators on EventTrackerService {
EventTrackerService sampled(double rate) => SampledEventTracker(this, rate);
EventTrackerService except(bool Function(Event event) excluded) =>
ExceptEventTracker(this, excluded);
}
/** Sends only some events, for a vendor that charges per event. */
export class SampledEventTracker implements EventTrackerService {
constructor(
private readonly inner: EventTrackerService,
private readonly rate: number,
private readonly random: () => number = Math.random,
) {}
async track(event: Event): Promise<void> {
if (this.random() < this.rate) await this.inner.track(event);
}
}
export class ExceptEventTracker implements EventTrackerService {
constructor(
private readonly inner: EventTrackerService,
private readonly excluded: (event: Event) => boolean,
) {}
async track(event: Event): Promise<void> {
if (!this.excluded(event)) await this.inner.track(event);
}
}
Then register the wrapped tracker instead of the original:
Herald has no filtering or sampling of its own, on purpose. Wrappers can be combined, work with any provider, and need nothing new from Herald.
A full vendor module¶
For a vendor you'll use seriously, with its own types, consent and identity, write a module shaped like Herald's own:
- factory interfaces, and composites that check the generic factory comes last;
- a generic factory and a required-mapping factory, like
GenericFirebaseEventTrackerFactoryandRequireMappedFirebaseEventTrackerFactory; - one tracker per distinct vendor call;
- services for lifecycle, identity and consent.
Each SDK's checklist for a new vendor lists every piece:
Android,
Flutter,
React Native.
On iOS, copy one of the vendor packages, such as
herald-ios-mixpanel. They're written for
vendors added to Herald, but work the same for one that lives in your app.