analytics.h
Analytics: events a plugin decides to record, and the destinations it writes itself to send them somewhere.
#include <sonore/analytics.h>- 4types
- 1other declaration
- Guides: Testing your plugin
On this page
Design notes
NOTHING IS SENT BY DEFAULT
Read this first, because it is the whole design. Including this header sends nothing, logs nothing and opens no connection. Analytics starts with no destinations at all, so every logEvent() is dropped on the floor until a plugin adds one -- and the SDK ships no destination that talks to a network. ThreadedAnalyticsDestination batches and retries, but its send is a pure virtual the plugin author writes: what leaves the user's machine, where it goes, and over what, is code in the plugin, where a reviewer can read it.
That is deliberate rather than an omission. A generated plugin that phoned home on its own would be a liability for whoever ships it -- a privacy policy it does not know it needs, a firewall prompt in the middle of a session, a support thread about why a reverb talks to a server. So the shape here is the reference's (a singleton, an event with parameters, a destination interface, a threaded batching base, a button tracker), and the policy is the opposite of "on unless you turn it off": off unless you write the part that turns it on. setSuspended(true) is there for the user's own opt-out switch on top of that.
Threads
logEvent() may be called from any thread EXCEPT the audio thread: it takes a lock and copies strings. A plugin that wants to count something that happens in process() counts it in an atomic and logs the count from the editor's timer.
Destinations are called under Analytics' lock, on the logging thread, so a destination's logEvent() must be quick and must not call back into Analytics. ThreadedAnalyticsDestination's is a push onto a queue; the slow part -- the send -- happens on its own thread.
Unloading
Stop a ThreadedAnalyticsDestination (stopAnalyticsThread(), or remove it from Analytics) before the plugin's module unloads. Left to the singleton's static destructor it would be joined while the OS holds the loader lock, and on Windows a thread cannot finish exiting while that lock is held: the host hangs on FreeLibrary. That is not a bug this header can fix from the inside, so it is written here where the person who can avoid it will see it.
Types 4
One recorded event, as a destination receives it.
Where events go. Implement logEvent(); see the threading note above.
The event log a plugin writes to, and the destinations it fans out to.
instance() is the shared one the reference's singleton corresponds to. The constructor is public as well, because a test -- or a plugin that keeps two independent streams -- wants one that nothing else can write into.
class ThreadedAnalyticsDestination : public AnalyticsDestination
A destination that queues events and hands them over in batches, from a thread of its own, to a send the plugin writes.
What it does so the send does not have to:
- BATCHES. Events collect for batchPeriodMs and go out up to maxBatchSize at a time: one request a second rather than one per click.
- RETRIES. A send that returns false (or throws) puts its batch back at the front of the queue, in order, and the next attempt waits twice as long as the last, up to maxRetryDelayMs. The first success resets the wait. A machine that is offline costs one failed attempt per five minutes, not one per second.
- RATE LIMITS. A bucket of maxEventsPerMinute tokens, refilled at that rate: a plugin that logs from a loop by mistake is cut off at a minute's worth rather than filling the queue, the disk and somebody's server. Dropped events are counted, not hidden.
- BOUNDS THE QUEUE. Past maxQueuedEvents the OLDEST are dropped, because the newest describe what the user is doing now.
- PERSISTS. stopAnalyticsThread() writes whatever is unsent to unsentEventsFile, and startAnalyticsThread() reads it back and deletes it, so a session that ends offline is sent by the next one. Override saveUnloggedEvents/restoreUnloggedEvents to keep them elsewhere.
Time comes from a clock the caller can replace (setClock), and one pass of the thread's loop is a public call (processPendingEvents), so every one of the behaviours above can be tested without sleeping.
LIFETIME, the one rule a subclass must follow: call stopAnalyticsThread() in the SUBCLASS's destructor. The thread calls logBatchedEvents(), which the subclass implements; by the time this base destructor runs the subclass is gone, and a thread still sending would call into a destroyed object. The base destructor stops the thread too, as a last resort, but it cannot make that call safe.
Aliases 1
- using
using AnalyticsParameters = std::map<std::string, std::string>Named values attached to an event or to the user. Sorted by key, so two events with the same parameters serialise identically.analytics.h:73