piano_composer 1.0.4
piano_composer: ^1.0.4 copied to clipboard
Piano Composer Flutter SDK.
Piano Composer SDK for Flutter #
Piano Composer Flutter SDK delivers personalized experiences, paywalls, and A/B tests from Piano's experience management platform, and reports user interactions back for attribution.
Installation #
Add the dependency to your pubspec.yaml:
dependencies:
piano_composer: ^1.0.4
Getting Started #
piano_composer builds on piano_common,
so you need a Piano instance first:
import 'package:piano_common/piano_common.dart';
import 'package:piano_composer/piano_composer.dart';
final piano = await Piano.init(
endpoint: PianoEndpoint.production,
aid: '<AID>',
);
Get a client via piano.composer(). Each call returns a new
PianoComposerClient, so retain the reference when you need to share
configuration or read back state (browserId, pageViewId) after a request:
final composer = piano.composer();
final request = PianoComposerRequest(
url: 'https://piano.io/article',
title: 'Article Title',
tags: ['technology', 'news'],
);
final response = await composer.execute(request);
Configuration #
Configuration setters are chainable and return the same client:
final composer = piano.composer()
..userToken('<access_token>') // token for personalized experiences
..gaClientId('<ga_client_id>') // link experiences to Google Analytics
..browserIdProvider(() => '<browser_id>'); // supply a custom browser id
Experience Interceptors #
Extend PianoComposerInterceptor to hook into the request lifecycle. Both methods
are optional:
class CustomInterceptor extends PianoComposerInterceptor {
@override
void beforeExecute(PianoComposerRequest request) {
// Inspect the request before it is sent, or throw to refuse it.
}
@override
void afterExecute(
PianoComposerRequest request,
PianoComposerResponse response,
) {
// Handle the response. `response.pageViewId` is the page view Composer
// reported this request under, so another SDK can be given the same one.
}
}
final composer = piano.composer()
..addExperienceInterceptor(CustomInterceptor());
beforeExecute is the only place an interceptor can fail an experience: throwing
there refuses the request before it is sent. The experience has been executed
successfully by the time afterExecute runs, so throwing there does not fail it —
the failure is logged and the remaining interceptors still run.
That applies to what afterExecute throws before it returns. It returns void,
so an async implementation hands the failures that happen after its first
await to the surrounding zone instead of to the client. Start the work that has
to wait and handle it yourself:
@override
void afterExecute(
PianoComposerRequest request,
PianoComposerResponse response,
) {
unawaited(
report(response).catchError((Object error) => log(error)),
);
}
response.pageViewId belongs to the response being handled rather than to the
client, so it is the right page view even while clients of the same instance
execute in parallel, and the work started above reads the same value whenever it
runs.
An interceptor that joins Composer with another SDK can also name the browser id
of the requests it intercepts. It is passed next to the interceptor, so that
PianoComposerInterceptor stays the two methods above:
final composer = piano.composer()
..addExperienceInterceptor(
interceptor,
browserIdProvider: () => '<browser_id>', // null leaves the browser id alone
);
Interceptors of the Instance #
piano.composer() returns a new client on every call, so an interceptor added to
one client is unknown to the next one. Enable it for the instance instead when
every experience of the application is meant to run it. This lives in its own
library, since executing an experience needs none of it:
import 'package:piano_composer/interceptors.dart';
piano.composerInterceptors.enable(CustomInterceptor());
Every client of the instance runs it, including the clients created before it was enabled, because the interceptors are read for every request:
- Interceptors of the client run before the enabled ones. Enabling the same
interceptor twice does nothing, and
disabletakes one back, reporting whether it was enabled. piano.composerInterceptors.enabledlists what is enabled for the instance, and one instance shares nothing with another.enabletakes abrowserIdProvideras well, which is the browser id the requests of the instance report while that interceptor is enabled. The first interceptor that knows one names it, the interceptors of the client are asked before the enabled ones, and abrowserIdProvidergiven to the client wins over all of them.piano.composerInterceptors.browserId()reports the one the enabled interceptors currently name.
This is also how a package built on Composer is turned on — see
piano_c1x, which is enabled once for the
instance rather than once per client.
Building a Request #
PianoComposerRequest carries the content metadata and targeting context.
All fields are optional:
| Field | Type | Description |
|---|---|---|
url |
String? |
Page URL |
referer |
String? |
Referring URL |
title |
String? |
Content title |
description |
String? |
Content description |
tags |
List<String> |
Content tags |
keywords |
List<String> |
Content keywords |
zone |
String? |
Targeting zone |
contentId |
String? |
Content identifier |
contentType |
String? |
Content type |
contentCreated |
String? |
ISO 8601 timestamp (use PianoComposerRequest.formatDate) |
contentAuthor |
String? |
Content author |
contentSection |
String? |
Content section |
contentIsNative |
bool? |
Whether the content is native |
customVariables |
Map<String, List<String>?> |
Custom targeting variables |
isDebug |
bool |
Enable verbose API responses |
final request = PianoComposerRequest(
url: 'https://piano.io/article/123',
title: 'Breaking News Article',
description: 'Latest updates on technology',
contentId: '123',
contentType: 'article',
contentCreated: PianoComposerRequest.formatDate(DateTime.now()),
contentAuthor: 'John Doe',
contentSection: 'Technology',
zone: 'homepage',
customVariables: {
'user_type': ['premium'],
'region': ['us-west'],
},
);
final response = await composer.execute(request);
Handling Events #
execute returns a PianoComposerResponse whose result.events contains the
experience events. Handle them with typed listeners:
final response = await composer.execute(
request,
listeners: [
PianoComposerEventTypeListener<PianoComposerShowTemplate>(
onEvent: (event) => print('Show template: ${event.eventData.url}'),
),
PianoComposerEventTypeListener<PianoComposerMeter>(
onEvent: (event) => print('Meter event received'),
),
],
);
Or receive every event with a single callback:
final response = await composer.execute(
request,
onEvents: (events) {
for (final event in events) {
print('Event: ${event.eventData.runtimeType}');
}
},
);
Event types include PianoComposerExperienceExecute, PianoComposerMeter,
PianoComposerShowTemplate, PianoComposerShowForm,
PianoComposerShowLogin, PianoComposerShowRecommendations,
PianoComposerUserSegment, PianoComposerSetResponseVariable, and
PianoComposerNonSite.
Tracking #
Report user interactions back to Piano through a PianoComposerTracking
instance so they are attributed to the originating experience:
final tracking = piano.composer().tracking;
// User dismissed a template or experience.
await tracking.trackCloseEvent('<tracking_id>');
// A recommendations widget was shown, then clicked.
await tracking.trackRecommendationsDisplay('<tracking_id>');
await tracking.trackRecommendationsClick('<tracking_id>', '<url>');
// A custom form was shown, then submitted.
await tracking.trackCustomFormImpression('<form_name>', '<tracking_id>');
await tracking.trackCustomFormSubmission('<form_name>', '<tracking_id>');
Every event received from execute also carries its own tracking reference
and tracking id, so there is no need to keep the client around:
await composer.execute(
request,
listeners: [
PianoComposerEventTypeListener<PianoComposerShowTemplate>(
onEvent: (event) => event.tracking?.trackCloseEvent(
event.eventExecutionContext.trackingId,
),
),
],
);
tracking is null only for events you construct yourself (for example via
PianoComposerEvent.fromJson), because the client attaches it while processing
the response.
Reading Response Data #
// Retain the same instance to read back state after execute().
final composer = piano.composer();
final response = await composer.execute(request);
// Cookies
final tbCookie = response.tbCookie; // tracking cookie
final xbCookie = response.xbCookie; // experience cookie
final taCookie = response.taCookie; // analytics cookie
// Identifiers
final browserId = composer.browserId;
final pageViewId = response.pageViewId; // the page view of this very request
final userId = response.userId;
// Edge cookies (if enabled)
final edgeCookies = composer.edgeCookies;
Clearing Stored Data #
Clear all locally stored data and cookies:
piano.composer().clearStoredData();
Error Handling #
execute throws a PianoException when the API returns errors:
try {
final response = await piano.composer().execute(request);
} on PianoException catch (e) {
print('Piano error: ${e.message}');
} catch (e) {
print('Unexpected error: $e');
}