Skip to content
This repository has been archived by the owner on Dec 9, 2024. It is now read-only.

Commit

Permalink
Merge pull request #817 from solocommand/idx-idt-tracking
Browse files Browse the repository at this point in the history
IdentityX identity tracking
  • Loading branch information
solocommand authored Oct 30, 2023
2 parents f888e65 + 4738cd9 commit 5aa6f4a
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const gql = require('graphql-tag');
const userFragment = require('../fragments/active-user');

module.exports = gql`
query LoadUserByExternalId(
$identifier: AppUserExternalIdentifierInput!,
$namespace: AppUserExternalNamespaceInput!
) {
appUserByExternalId(input: { identifier: $identifier, namespace: $namespace }) {
...ActiveUserFragment
}
}
${userFragment}
`;
21 changes: 18 additions & 3 deletions packages/marko-web-identity-x/middleware.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const { asyncRoute } = require('@parameter1/base-cms-utils');
const IdentityX = require('./service');

/**
Expand All @@ -6,9 +7,23 @@ const IdentityX = require('./service');
* @param {IdentityXConfiguration} config The IdentityX config object
* @returns {function} The middleware function
*/
module.exports = (config) => (req, res, next) => {
module.exports = (config) => asyncRoute(async (req, res, next) => {
const service = new IdentityX({ req, res, config });
req.identityX = service;
res.locals.identityX = service;
next();
};

const cookie = service.getIdentity(res);

// Don't overwrite an existing cookie
if (cookie) return next();

// Set cookie for logged in users
if (service.token) {
const { user } = await service.loadActiveContext();
if (user && user.id) {
service.setIdentityCookie(user.id);
}
}

return next();
});
1 change: 1 addition & 0 deletions packages/marko-web-identity-x/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"apollo-link-http": "^1.5.17",
"body-parser": "^1.20.1",
"cookie": "0.3.1",
"debug": "^4.3.4",
"dayjs": "^1.11.7",
"express": "^4.18.2",
"graphql-tag": "^2.12.6",
Expand Down
1 change: 1 addition & 0 deletions packages/marko-web-identity-x/routes/authenticate.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ module.exports = asyncRoute(async (req, res) => {
});
tokenCookie.setTo(res, authToken.value);
contextCookie.setTo(res, { loginSource });
identityX.setIdentityCookie(user.id);
res.json({
ok: true,
user,
Expand Down
57 changes: 57 additions & 0 deletions packages/marko-web-identity-x/service.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
const { get, getAsObject } = require('@parameter1/base-cms-object-path');
const debug = require('debug')('identity-x');
const createClient = require('./utils/create-client');
const getActiveContext = require('./api/queries/get-active-context');
const checkContentAccess = require('./api/queries/check-content-access');
const loadUser = require('./api/queries/load-user');
const appUserByExternalIdQuery = require('./api/queries/load-user-by-external-id');
const addExternalUserId = require('./api/mutations/add-external-user-id');
const setCustomAttributes = require('./api/mutations/set-custom-attributes');
const impersonateAppUser = require('./api/mutations/impersonate-app-user');
Expand All @@ -15,6 +17,7 @@ const callHooksFor = require('./utils/call-hooks-for');

const isEmpty = (v) => v == null || v === '';
const isFn = (v) => typeof v === 'function';
const IDENTITY_COOKIE_NAME = '__idx_idt';

class IdentityX {
constructor({
Expand Down Expand Up @@ -117,6 +120,60 @@ class IdentityX {
return access;
}

/**
* Returns the identity id from the request or supplied response.
*
* @returns {String}
*/
getIdentity(res) {
try {
const id = get(this.req, `cookies.${IDENTITY_COOKIE_NAME}`);
if (id) return id;
const sc = res.get('set-cookie');
const scv = (typeof sc === 'string' ? [sc] : sc || []).reduce((o, c) => {
const [r] = `${c}`.split(';');
const [k, v] = `${r}`.split('=');
return { ...o, [k]: v };
}, {});
if (scv) return get(scv, IDENTITY_COOKIE_NAME);
} catch (e) {
debug('Unable to parse identity', e);
}
return null;
}

/**
* Sets the IdentityX Identity cookie to the response
*/
setIdentityCookie(id) {
const options = {
maxAge: 60 * 60 * 24 * 365,
httpOnly: false,
};
this.res.cookie(IDENTITY_COOKIE_NAME, id, options);
}

/**
* @param {Object} o
* @param {String} o.identifier
* @param {Object} o.namespace
*
* @returns {Promise<Object>}
*/
async findUserByExternalId({ identifier, namespace }) {
const apiToken = this.config.getApiToken();
if (!apiToken) throw new Error('Unable to add external ID: No API token has been configured.');
const { data } = await this.client.query({
query: appUserByExternalIdQuery,
variables: {
identifier: { value: identifier },
namespace,
},
context: { apiToken },
});
return data.appUserByExternalId;
}

/**
*
* @param {object} params
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const gql = require('graphql-tag');

module.exports = gql`
query IdXIdentifyCustomer($id: String!) {
customerByEncryptedId(input: { id: $id, errorOnNotFound: false }) {
id
primaryEmailAddress { emailAddress }
}
}
`;
2 changes: 2 additions & 0 deletions packages/marko-web-omeda-identity-x/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const setPromoSourceCookie = require('./middleware/set-promo-source');
const stripOlyticsParam = require('./middleware/strip-olytics-param');
const resyncCustomerData = require('./middleware/resync-customer-data');
const setOlyticsCookie = require('./middleware/set-olytics-cookie');
const setIdentityCookie = require('./middleware/set-identity-cookie');
const rapidIdentify = require('./middleware/rapid-identify');
const rapidIdentifyRouter = require('./routes/rapid-identify');
const props = require('./validation/props');
Expand Down Expand Up @@ -158,6 +159,7 @@ module.exports = (app, params = {}) => {
identityX(app, idxConfig, { templates: idxRouteTemplates });

app.use(setOlyticsCookie({ brandKey }));
app.use(setIdentityCookie({ brandKey }));

// install the Omeda data sync middleware
app.use(resyncCustomerData({
Expand Down
12 changes: 12 additions & 0 deletions packages/marko-web-omeda-identity-x/middleware/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Omeda+IdentityX Middleware
===

## Set Identity Cookie
This [middleware](./set-identity-cookie.js) sets the IdentityX Identity cookie. This cookie represents the identity that all user behavior should be attributed to.

Test cases:
1. Fully anonymous user (no omeda or idx tokens). No cookie should be set.
2. Logged in user w/o cookie: cookkie should be set
3. Logged in user w/ cookie: Cookie shoudl not be set.
4. Omeda identity cookie present, identityx user exists for ext id: cookie shoul dbe set.
5. Omeda identity cookie present, no identityx user exists: cookie should be set.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const { asyncRoute } = require('@parameter1/base-cms-utils');
const { get } = require('@parameter1/base-cms-object-path');
const olyticsCookie = require('@parameter1/base-cms-marko-web-omeda/olytics/customer-cookie');
const query = require('../api/queries/customer-by-encrypted-id');

/**
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @returns {String|null}
*/
const findOlyticsId = (req, res) => {
// Read from request cookies
const reqId = olyticsCookie.parseFrom(req);
if (reqId) return reqId;
// request query param
const { oly_enc_id: qId } = req.query;
if (qId) return qId;
// response cookie
const cookies = (res.get('set-cookie') || []).reduce((o, c) => {
const [r] = `${c}`.split(';');
const [k, v] = `${r}`.split('=');
return { ...o, [k]: v };
}, {});
const resId = olyticsCookie.parseFrom({ cookies });
return resId;
};

/**
* @typedef OIDXRequest
* @prop {import('@parameter1/omeda-graphql-client')} $omedaGraphQLClient
* @prop {import('@parameter1/base-cms-marko-web-identity-x/service')} identityX
*
* @param {Object} o
* @param {String} o.brandKey
*/
module.exports = ({
brandKey,
}) => asyncRoute(async (req, res, next) => {
/** @type {OIDXRequest} */
const { identityX: idx, $omedaGraphQLClient: omeda } = req;
const cookie = idx.getIdentity(res);

// Don't overwrite an existing cookie
if (cookie) return next();

// get oly enc id. if we don't have one, bail
const omedaId = findOlyticsId(req, res);
if (!omedaId) return next();

// Look up idx user by encrypted id
const namespace = { provider: 'omeda', tenant: brandKey.toLowerCase(), type: 'customer' };
const identity = await idx.findUserByExternalId({ identifier: omedaId, namespace });
if (identity) {
idx.setIdentityCookie(identity.id);
return next();
}

const or = await omeda.query({ query, variables: { id: omedaId } });
const email = get(or, 'data.customerByEncryptedId.primaryEmailAddress.emailAddress');
if (email) {
// Upsert the user and add the external id to it.
const { id } = await idx.createAppUser({ email });
await idx.addExternalUserId({
userId: id,
identifier: { value: omedaId, type: 'encrypted' },
namespace,
});
idx.setIdentityCookie(id);
return next();
}
return next();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
$ const { req, res } = out.global;
$ const { identityX } = req;
$ const identity = identityX.getIdentity(res);

<if(Boolean(identityX))>
<marko-web-identity-x-context|{ user, hasUser }|>
$ const payload = {
...(identity && { identity }),
...(hasUser && {
user: identityX.config.getGTMUserData(user),
user_id: user.id
}),
};
<if(hasUser || identity)>
<marko-web-gtm-push data=payload />
</if>
</marko-web-identity-x-context>
</if>
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
"<identity-x-newsletter-form-inline>": {
"template": "./newsletter-inline.marko"
},
"<identity-x-identify>": {
"template": "./identify.marko"
},
"<identity-x-newsletter-form-footer>": {
"template": "./newsletter-footer.marko"
}
Expand Down
10 changes: 8 additions & 2 deletions packages/web-common/website-context.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const gql = require('graphql-tag');
const siteFragment = require('./graphql/website-context-fragment');

const { error } = console;
const query = gql`
query MarkoWebsiteContext {
Expand All @@ -17,6 +18,11 @@ ${siteFragment}
* @param {ApolloClient} apolloClient The BaseCMS Apollo GraphQL client that will perform the query.
*/
module.exports = async (apolloClient) => {
const { data } = await apolloClient.query({ query });
return data.websiteContext;
try {
const { data } = await apolloClient.query({ query });
return data.websiteContext;
} catch (e) {
error(e);
throw e;
}
};
2 changes: 2 additions & 0 deletions services/example-website/server/components/document.marko
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ $ const { nativeX, cdn, contentMeterState } = out.global;
target-tag="body"
/>

<identity-x-identify />

<${input.head} />

<marko-web-gam-enable />
Expand Down

0 comments on commit 5aa6f4a

Please sign in to comment.