Node.js client
This library makes it easy to interact with the Hull API, send tracking and properties and handle Server-side Events we send to installed Ships.
Usage
import Hull from 'hull';
const hull = new Hull({
id: 'HULL_ID',
secret: 'HULL_SECRET',
organization: 'HULL_ORGANIZATION_DOMAIN'
});
Creating a new Hull client is pretty straightforward.
In Ship Events, we create and scope one for you to abstract the lifecycle
Calling the API
const params = {}
hull.get(path, params).then(function(data){
console.log(response);
},function(err, response){
console.log(err);
});
Once you have instanciated a client, you can use one of the get
, post
,
put
or delete
methods to perform actions of our APIs.
The first parameter is the route, the second is the set of parameters you want
to send with the request. They all return Promises so you can use the .then()
syntax if you're more inclined.
Instance Methods
hull.configuration()
Returns the global configuration
hull.userToken()
hull.userToken({email:'xxx@example.com',name:'FooBar'}, claims)
Used for Bring your own users.
Creates a signed string for the user passed in hash. userHash
needs an email
field.
You can then pass this client-side to Hull.js to authenticate users client-side and cross-domain
hull.currentUserId()
hull.currentUserId(userId, userSig)
Checks the validity of the signature relatively to a user id
hull.currentUserMiddleware()
const app = express();
app.use(hull.currentUserMiddleware);
app.use(function(req,res,next){
console.log(req.hull.userId)
})
Reverse of Bring your own Users. When using Hull's Identity management, tells you who the current user is. Generates a middleware to add to your Connect/Express apps.
Utils
hull.utils.groupTraits(user_report)
const hull = new Hull({config});
hull.utils.groupTraits({
'email': 'romain@user',
'name': 'name',
'traits_coconut_name': 'coconut',
'traits_coconut_size': 'large',
'traits_cb/twitter_bio': 'parisian',
'traits_cb/twitter_name': 'parisian',
'traits_group/name': 'groupname',
'traits_zendesk/open_tickets': 18
});
{
'id' : '31628736813n1283',
'email': 'romain@user',
'name': 'name',
'traits': {
'coconut_name': 'coconut',
'coconut_size': 'large'
},
cb: {
'twitter_bio': 'parisian',
'twitter_name': 'parisian'
},
group: {
'name': 'groupname',
},
zendesk: {
'open_tickets': 18
}
};
The Hull API returns traits in a "flat" format, with '/' delimiters in the key.
The Events handler Returns a grouped version of the traits in the flat user report we return from the API.
The NotifHandler already does this by default.
Impersonating a User
var user = hull.as({ anonymous_id: '123456789' });
var user = hull.as({ external_id: 'dkjf565wd654e' });
var user = hull.as('5718b59b7a85ebf20e000169', false);
user.get('/me').then(function(me){
console.log(me)
});
user.userToken();
One of the more frequent use case is to perform API calls with the identity of a given user. We provide several methods to do so.
You can use an internal Hull id
, an Anonymous ID from that we call a anonymous_id
, an ID from your database that we call external_id
, or even the ID from a supported social service such as Instagram;
Assigning the user
variable doesn't make an API call, it scopes the calls to another instance of hull
client. This means user
is an instance of the hull
client scoped to this user.
The second parameter lets you define whether the calls are perform with Admin rights or the User's rights.
Return a hull client
scoped to the user identified by it's Hull ID. Not lazily created. Needs an existing User
hull.as(userId)
Return a hull client
scoped to the user identified by it's Social network ID. Lazily created if Guest Users are enabled
hull.as('instagram|facebook|google:userId', sudo)
Return a hull client
scoped to the user identified by it's External ID (from your dashboard). Lazily created if Guest Users are enabled
hull.as({external_id:'externalId'}, sudo)
Return a hull client
scoped to the user identified by it's External ID (from your dashboard). Lazily created if Guest Users are enabled
hull.as({anonymous_id:'anonymousId'}, sudo)
Return a hull client
scoped to the user identified by only by an anonymousId. Lets you start tracking and storing properties from a user before you have a UserID ready for him. Lazily created if Guest Users are enabled
When you have a UserId, just pass both to link them.
hull.as({email:'user@email.com'}, sudo)
Methods for user-scoped instances
const sudo = true;
const userId = '5718b59b7a85ebf20e000169';
const externalId = 'dkjf565wd654e';
const anonymousId = '44564-EJVWE-1CE56SE-SDVE879VW8D4';
const user = hull.as({external_id: externalId, anonymous_id: anonymousId})
When you do this, you get a new client that has a different behaviour. It's now behaving as a User would. It means it does API calls as a user and has new methods to track and store properties
user.track(event, props, context)
user.track('new support ticket', { messages: 3,
priority:'high'
}, {
source: 'zendesk',
ip: null,
referer: null,
created_at: '2013-02-08 09:30:26.123+07:00'
});
Stores a new event, which you can namespace using the source
property in the context
parameter
user.traits(properties, context)
user.traits({
opened_tickets: 12
}, { source: 'zendesk' });
Stores Properties on the user.
If you need to be sure the properties are set immediately on the user, you can use the context param { sync: true }
.
user.traits({
fetched_at: new Date().toISOString()
}, { source: 'mailchimp', sync: true });
Class Methods
Logging Methods: Hull.logger.debug(), Hull.logger.info() ...
Hull.logger.info("message", { object });
hull.logger.info("message", { object });
Hull.logger.info("message", { object });
hull.logger.info("message", { object });
import winstonSlacker from "winston-slacker";
Hull.logger.add(winstonSlacker, { ... });
Uses Winston
The Logger comes in two flavors, Hull.logger.xxx
and hull.logger.xxx
- The first one is a generic logger, the second one injects the current instance of Hull
so you can retreive ship name, id and organization for more precision.
NotifHandler()
NotifHandler is a packaged solution to receive User and Segment Notifications from Hull. It's built to be used as an express route. Hull will receive notifications if your ship's manifest.json
exposes a subscriptions
key:
{
"subscriptions" : [ { "url" : "/notify" } ]
}
Here's how to use it.
const app = express();
import { NotifHandler } from 'hull';
const handler = NotifHandler({
hostSecret: hostSecret
onSubscribe() {}
onError() {}
handlers: {
groupTraits: true,
'event': function() {
console.log('Event Handler here', notif, context);
},
'ship:update': function(notif, context){},
'segment:update': function(notif, context){},
'segment:delete': function(notif, context){},
'user:delete': function(notif, context){},
'user:create': function(notif, context){},
'user:update' : function(notif, context) {
console.log('Event Handler here', notif, context);
}
}
})
app.post('/notify', handler);
Your app can subscribe to events from Hull and receive Events via http POST.
For this we provide a helper called NotifHandler that handles all the complexity of subscribing to events and routing them to specific methods. All you need to do is declare which methods handle what Events.
Example of user:update
payload
{
"user": {
"id": "572f63eb8c35fc5d4300034e",
"anonymous_ids": [ "1462723549-f16cea7e-6a7d-4ba5-b506-c16bfd43ebbe" ],
"created_at": "2016-05-08T16:06:04Z",
"name": "Romain Dardour",
"first_name": "Romain",
"last_name": "Dardour",
"domain": "hull.io",
"email": "romain@hull.io",
"phone": "+33600000000",
"picture": "https://d1ts43dypk8bqh.cloudfront.net/v1/avatars/a63f299c-4fbb-4c2e-8d7e-8b4af888f890",
"accepts_marketing": false,
"address_city": "Paris",
"address_country": "France",
"address_state": "Île-de-France",
"last_seen_at": "2017-01-10T16:26:25Z",
"last_known_ip": "54.227.22.135",
"first_seen_at": "2016-09-28T13:19:59Z",
"first_session_initial_referrer": "",
"first_session_initial_url": "https://hull-2.myshopify.com/",
"first_session_platform_id": "561fb665450f34b1cf00000f",
"first_session_started_at": "2016-09-28T13:19:59Z",
"latest_session_initial_referrer": "https://hull-2.myshopify.com/",
"latest_session_initial_url": "https://hull-2.myshopify.com/account/login",
"latest_session_platform_id": "561fb665450f34b1cf00000f",
"latest_session_started_at": "2016-10-25T10:15:34Z",
"signup_session_initial_referrer": "",
"signup_session_initial_url": "https://hull-2.myshopify.com/",
"signup_session_platform_id": "561fb665450f34b1cf00000f",
"signup_session_started_at": "2016-09-28T13:19:59Z",
"traits": {
"usage_score" : 89.5
},
"hubspot": {
"associated_deals_count": "1",
"became_opportunity_at": "2016-09-09T07:04:36+00:00",
"created_at": "2016-09-09T07:01:01+00:00",
"email": "romain@hull.io",
"fetched_at": "2017-01-10T16:40:30Z",
"first_deal_created_at": "2016-09-28T13:24:35+00:00",
"first_name": "Romain",
"job_title": "COO",
"last_name": "Dardour",
"lifecycle_stage": "opportunity",
"recent_deal_amount": "",
"updated_at": "2017-01-10T16:37:55+00:00"
}
},
"segments": [
{
"id": "57adda830ffa84da28000083",
"name": "Dudes called Romain",
"type": "users_segment",
"created_at": "2016-08-12T14:17:39Z",
"updated_at": "2016-10-21T07:39:01Z"
},
{
"id": "572091bf13440a016c00002b",
"name": "Views Products Frequently",
"type": "users_segment",
"created_at": "2016-04-27T10:17:35Z",
"updated_at": "2016-12-01T10:51:24Z"
}
],
"events": [
{
"context": {
"location": {
"latitude": 48.8628,
"longitude": 2.3292
},
"page": {
"url": "https://hull-2.myshopify.com/products/suspendisse-congue-sodales-massa-sit-amet-euismod-aliquet-sapien-non-dictum"
}
},
"created_at": "2017-01-11T17:52:11Z",
"event": "Viewed Product",
"event_source": "track",
"event_type": "track",
"properties": {
"category": "luctus",
"id": 2986706563,
"name": "Black Cat Classic Espresso",
"price": 25
}
}
],
"changes": {
"user": {
"traits_hubspot/fetched_at": [ "2016-12-09T10:47:13Z", "2017-01-10T16:40:30Z" ],
"traits_hubspot/updated_at": [ "2016-12-09T10:46:03+00:00", "2017-01-10T16:37:55+00:00" ]
},
"segments": {
"entered": [
{
"id": "572091bf13440a016c00002b",
"name": "Views Products Frequently",
"type": "users_segment",
"created_at": "2016-04-27T10:17:35Z",
"updated_at": "2016-12-01T10:51:24Z"
}
],
"left": [
{
"created_at": "2016-02-03T10:47:07Z",
"id": "56b1daab5580c06798000051",
"name": "Approved users",
"type": "users_segment",
"updated_at": "2016-12-01T10:57:30Z"
}
]
},
"is_new": false
},
"event": "user:update",
"timestamp": "2017-01-10T16:41:00.831Z"
}
BatchHandler()
BatchHandler is a packaged solution to receive Batches of Users. It's built to be used as an express route. Hull will receive notifications if your ship's manifest.json
exposes a batch
tag in tags
:
{
"tags" : [ "batch" ]
}
Here is how to use it:
const app = express();
import { NotifHandler } from 'hull';
const handler = BatchHandler({
groupTraits: false,
handler: function(notifications=[], context) {
notifications.map(n => updateUser(n, context));
}
})
app.post('/batch', handler);
OAuthHandler()
OAuth Handler is a packaged authentication handler using Passport. You give it the right parameters, it handles the entire auth scenario for you.
It exposes hooks to check if the ship is Set up correctly, inject additional parameters during login, and save the returned settings during callback.
Here is how to use it:
import Hull from "hull";
import { Strategy as HubspotStrategy } from "passport-hubspot";
import { renderFile } from "ejs";
import express from "express";
app.set("views", `${__dirname}/../views`);
app.set("view engine", "ejs");
app.engine("html", renderFile);
app.use(express.static(path.resolve(__dirname, "..", "dist")));
app.use(express.static(path.resolve(__dirname, "..", "assets")));
const { OAuthHandler } = Hull;
app.use("/auth", OAuthHandler({
hostSecret,
name: "Hubspot",
tokenInUrl: true,
Strategy: HubspotStrategy,
options: {
clientID: "xxxxxxxxx",
clientSecret: "xxxxxxxxx",
scope: ["offline", "contacts-rw", "events-rw"]
},
isSetup(req, { /* hull,*/ ship }) {
if (!!req.query.reset) return Promise.reject();
const { token } = ship.private_settings || {};
return (!!token) ? Promise.resolve({ valid: true, total: 2}) : Promise.reject({ valid: false, total: 0});
},
onLogin: (req, { hull, ship }) => {
req.authParams = { ...req.body, ...req.query };
return save(hull, ship, {
portalId: req.authParams.portalId
});
},
onAuthorize: (req, { hull, ship }) => {
const { refreshToken, accessToken } = (req.account || {});
return save(hull, ship, {
refresh_token: refreshToken,
token: accessToken
});
},
views: {
login: "login.html",
home: "home.html",
failure: "failure.html",
success: "success.html"
},
}));
manifest.json
{
"admin" : "/auth/",
}
Params:
hostSecret
The ship hosted secret (Not the one received from Hull. The one the hosted app itself defines. Will be used to encode tokens).
name
The name displayed to the User in the various screens.
tokenInUrl
Some services (like Stripe) require an exact URL match.
Some others (like Hubspot) don't pass the state back on the other hand.
Setting this flag to false (default: true) removes the token
Querystring parameter in the URL to only rely on the state
param.
Strategy
A Passport Strategy.
options
An options hash passed to Passport to configure the OAuth Strategy. (See Passport OAuth Configuration)
isSetup()
A method returning a Promise, resolved if the ship is correctly setup, or rejected if it needs to display the Login screen.
Lets you define in the Ship the name of the parameters you need to check for.
You can return parameters in the Promise resolve and reject methods, that will be passed to the view. This lets you display status and show buttons and more to the customer
onLogin()
A method returning a Promise, resolved when ready.
Best used to process form parameters, and place them in req.authParams
to be submitted to the Login sequence. Useful to add strategy-specific parameters, such as a portal ID for Hubspot for instance.
onAuthorize()
A method returning a Promise, resolved when complete.
Best used to save tokens and continue the sequence once saved.
views
Required, A hash of view files for the different screens.
Each view will receive the following data:
views: {
login: "login.html",
home: "home.html",
failure: "failure.html",
success: "success.html"
}
{
name: "The name passed as handler",
urls: {
login: '/auth/login',
success: '/auth/success',
failure: '/auth/failure',
home: '/auth/home',
},
ship: ship
}
Middlewares
Hull.Middlewares.hullClient()
import Hull from "hull";
const hullClient = Hull.Middlewares.hullClient;
app.use(hullClient({ hostSecret:"supersecret", fetchShip: true, cacheShip: true }));
app.use((req, res) => { res.json({ message: "thanks" }); });
app.use(function(err, res, req, next){
if(err) return res.status(err.status || 500).send({ message: err.message });
});
This middleware standardizes the instanciation of a Hull client from configuration passed as a Query string or as a token. It also optionally fetches the entire ship's configuration and caches it to save requests.
Here is what happens when your express app receives a query.
- If a config object is found in
req.hull.config
it will be used to create an instance of the client. - If a token is present in
req.hull.token
, the middleware will try to use the hostSecret
to decode it, store it in req.hull.client
. When using req.hull.token
, the decoded token should be a valid configuration object: {id, organization, secret}
- If the query string contains
id
, secret
, organization
, they will be stored in req.hull.config
- After this, if a valid configuration is in
req.hull.config
, a Hull client instance will be created and stored in req.hull.client
- When this is done, if
fetchShip=true
(default) then the Ship will be fetched and stored in req.hull.ship
- If
cacheShip=true
(default) the results will be cached. - If the configuration or the secret is invalid, an error will be thrown that you can catch using express error handlers.
app.use(function(req, res, next){
req.hull.token = myToken;
next();
})
app.use(hullClient({ hostSecret:"supersecret", fetchShip: true, cacheShip: true }));
app.use(function(req, res){
req.hull.config
req.hull.client
req.hull.ship
});
Routes
A simple set of route handlers to reduce boilerplate by a tiny bit.
Hull.Routes.Readme
import Hull from 'hull';
const { Routes } = Hull;
app.get("/readme", Routes.Readme);
app.get("/", Routes.Readme);
Hull.Routes.Manifest
import Hull from 'hull';
const { Routes } = Hull;
app.get("/manifest.json", Routes.Manifest(__dirname));