liquid_mixpanel
Mixpanel destination for
liquid_analytics — product analytics with People
profiles, Group Analytics, EU/India residency, and client-side opt-out.
Install
dependencies:
liquid_analytics: ^0.2.0
liquid_mixpanel: ^0.2.0
Use
Let the sink configure Mixpanel for you by passing a project token:
final liquid = Liquid(
sinks: [
MixpanelSink(
token: 'YOUR_PROJECT_TOKEN',
serverURL: 'https://api-eu.mixpanel.com', // optional residency / proxy
trackAutomaticEvents: false,
),
],
consent: ConsentPolicy(
requireOptIn: true,
map: {ConsentCategory.analytics: ['mixpanel']},
),
);
await liquid.init();
liquid.track('checkout_started', {
'cart_value': 42.0,
'is_gift': true,
'items': ['sku_1', 'sku_2'],
});
liquid.consent.grant(ConsentCategory.analytics);
If Mixpanel is already initialized elsewhere, inject the instance:
final mixpanel = await Mixpanel.init(
'YOUR_PROJECT_TOKEN',
trackAutomaticEvents: false,
);
final liquid = Liquid(sinks: [MixpanelSink(mixpanel: mixpanel)]);
Typed events (recommended)
The string API above is the quick path. A typed event keeps the name and
property keys in one place, and lets an event declare its own consent
category — useful when Mixpanel is mapped to marketing rather than
analytics:
class CheckoutStarted extends LiquidEvent {
const CheckoutStarted({required this.cartValue, required this.items});
final double cartValue;
final List<String> items;
@override
String get name => 'checkout_started';
@override
Map<String, Object?> get properties => {
'cart_value': cartValue,
'items': items, // nested lists/maps reach Mixpanel intact
};
}
class PromoTapped extends LiquidEvent {
const PromoTapped({required this.campaign});
final String campaign;
@override
String get name => 'promo_tapped';
@override
ConsentCategory get category => ConsentCategory.marketing;
@override
Map<String, Object?> get properties => {'campaign': campaign};
}
liquid.log(const CheckoutStarted(cartValue: 42, items: ['sku_1', 'sku_2']));
liquid.log(const PromoTapped(campaign: 'summer_sale')); // gated by marketing
No build step is involved — these are plain classes. To generate them from a
YAML schema instead, see
liquid_codegen.
Message mapping
| liquid message | Mixpanel call |
|---|---|
track |
track |
screen |
track as Screen Viewed with a screen_name property |
identify |
identify + People.set per trait |
group |
setGroup + MixpanelGroup.set per trait |
alias |
alias(alias, currentDistinctId) |
reset |
reset |
Mixpanel's group model uses a (groupKey, groupID) pair. liquid's single-id
group('acme') maps groupID = 'acme' with the key set by groupKey
('company' by default):
MixpanelSink(token: '…', groupKey: 'organization');
Screen mapping is configurable if you prefer a different event shape:
MixpanelSink(
token: '…',
screenEventName: 'Viewed Screen',
screenNameProperty: 'name',
);
Property handling
Mixpanel accepts nested objects, lists, booleans and numbers. MixpanelPropertyMapper
therefore only:
- drops
nullvalues, and - converts
DateTime(to ISO-8601) andEnum(to.name) recursively.
Group Analytics is the exception: MixpanelGroup.set only accepts string
values, so group traits are coerced to strings after conversion.
Opt-out bridge
Optionally mirror liquid's consent into Mixpanel's client-side opt-out so the SDK itself stops sending when the user denies analytics:
final mixpanelSink = MixpanelSink(token: '…');
final liquid = Liquid(sinks: [mixpanelSink]);
liquid.consent.addListener(() {
mixpanelSink.applyConsent(
granted: liquid.consent.statusOf(ConsentCategory.analytics) ==
ConsentStatus.granted,
);
});
Testing
MixpanelPropertyMapper is a pure transform and is unit-tested without a live
project. For end-to-end checks, run against a Mixpanel project and inspect the
Live View.
Libraries
- liquid_mixpanel
- Mixpanel destination for liquid_analytics.