Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for ECIDs passed via the adobe_mc query string parameter. Added appendIdentityToUrl command to enable cross-domain identity sharing. #862

Merged
merged 19 commits into from
May 12, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
07d5b7d
parse the query string for adobe_mc and add unit tests
jonsnyder Mar 18, 2022
797cc4b
Fixes after testing in the sandbox
jonsnyder Mar 18, 2022
7add720
Create functional tests and fix hasIdentity error in getIdentity payload
jonsnyder Mar 22, 2022
09a80bc
Merge remote-tracking branch 'origin/main' into queryStringIdentity
jonsnyder Mar 22, 2022
1277323
Add a few more functional tests, extract common functionality from tests
jonsnyder Mar 22, 2022
803f5c0
Add logging to AddQueryStringIdentityToPayload, add additional unit t…
jonsnyder Mar 22, 2022
1684ecf
Add better comments. Use constant for TTL
jonsnyder Apr 1, 2022
c1d7318
Write a different AMCV cookie if the ECID has changed. Add failing te…
jonsnyder Apr 1, 2022
0af3b6f
Add comment and update logging message from code review
jonsnyder Apr 1, 2022
acb4eb2
Add appendIdentityToUrl command with updated unit tests and sandbox.
jonsnyder Apr 1, 2022
cf9dbb5
Add functional tests for appendIdentityToUrl, round the timestamp
jonsnyder Apr 4, 2022
63838f5
Use alloyio2.com as the cross-domain link
jonsnyder Apr 4, 2022
e6b85f5
revert sandbox index.html back to original config
jonsnyder Apr 4, 2022
08c7d13
Remove unresolved promise error in test for Firefox and Safari
jonsnyder Apr 4, 2022
bdf2051
Add test from mobile team url
jonsnyder Apr 20, 2022
5638ee3
Add idOverwriteEnabled config
jonsnyder May 10, 2022
69a1340
Merge remote-tracking branch 'origin/main' into queryStringIdentity2
jonsnyder May 10, 2022
d394a21
Skip test that is waiting on Konductor changes
jonsnyder May 10, 2022
136f6e6
set idOverwriteEnabled default to false, Updated sandbox to help with…
jonsnyder May 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions sandbox/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import DualTag from "./DualTag";
import RedirectOffers from "./RedirectOffers";
import RedirectedNewPage from "./RedirectedNewPage";
import PersonalizationAnalyticsClientSide from "./PersonalizationAnalyticsClientSide";
import Identity from "./Identity";

function BasicExample() {
return (
Expand Down Expand Up @@ -74,6 +75,9 @@ function BasicExample() {
<li>
<a href="/redirectOffers">Redirect Offers</a>
</li>
<li>
<a href="/identity">Identity</a>
</li>
</ul>

<hr />
Expand All @@ -97,6 +101,7 @@ function BasicExample() {
<Route path="/dualTag" component={DualTag} />
<Route path="/redirectOffers" component={RedirectOffers} />
<Route path="/redirectedNewPage" component={RedirectedNewPage} />
<Route path="/identity" component={Identity} />
</div>
</Router>
);
Expand Down
60 changes: 60 additions & 0 deletions sandbox/src/Identity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
Copyright 2022 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

import React, { useState } from "react";

const getIdentity = setIdentity => () => {
window.alloy("getIdentity", { namespaces: ["ECID"] }).then(function(result) {
if (result.identity) {
console.log(
"Sandbox: Get Identity command has completed.",
result.identity.ECID
);
setIdentity(result.identity.ECID);
} else {
console.log(
"Sandbox: Get Identity command has completed but no identity was provided in the result (possibly due to lack of consent)."
);
setIdentity("No Identity");
}
});
};

const appendIdentityToUrl = event => {
const url = event.target.href;
event.preventDefault();
window.alloy("appendIdentityToUrl", { url }).then(({ url }) => {
document.location = url;
});
};

export default function Identity() {
const [identity, setIdentity] = useState("");

return (
<div>
<h1>Identity</h1>
<section>
<h2>Get Identity</h2>
<div>
<button onClick={getIdentity(setIdentity)}>Get ECID</button>
<h3>{identity}</h3>
</div>
</section>
<section>
<a href="https://alloyio2.com/identity" onClick={appendIdentityToUrl}>
Cross domain linked identity.
</a>
</section>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
Copyright 2022 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

import { objectOf, string } from "../../../utils/validation";
/**
* Verifies user provided event options.
* @param {*} options The user event options to validate
* @returns {*} Validated options
*/
export default objectOf({
url: string()
.required()
.nonEmpty()
})
.required()
.noUnknownFields();
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
Copyright 2022 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
const URL_REGEX = /^([^?#]*)(\??[^#]*)(#?.*)$/;

const getSeparator = queryString => {
if (queryString === "") {
return "?";
}
if (queryString === "?") {
return "";
}
return "&";
};

export default ({ dateProvider, orgId }) => (ecid, url) => {
const ts = Math.round(dateProvider().getTime() / 1000);
const adobemc = encodeURIComponent(`TS=${ts}|MCMID=${ecid}|MCORGID=${orgId}`);
const [, location, queryString, fragment] = url.match(URL_REGEX);
const separator = getSeparator(queryString);
return `${location}${queryString}${separator}adobe_mc=${adobemc}${fragment}`;
};
3 changes: 2 additions & 1 deletion src/components/Identity/configValidators.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { boolean } from "../../utils/validation";

const configValidators = {
thirdPartyCookiesEnabled: boolean().default(true),
idMigrationEnabled: boolean().default(true)
idMigrationEnabled: boolean().default(true),
idOverwriteEnabled: boolean().default(true)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think true is the right default?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think false might be a safer bet since that’s how it behaves today in Visitor.js

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let’s ask Mitch though

};

export default configValidators;
24 changes: 23 additions & 1 deletion src/components/Identity/createComponent.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { assign } from "../../utils";
import getIdentityOptionsValidator from "./getIdentity/getIdentityOptionsValidator";
import appendIdentityToUrlOptionsValidator from "./appendIdentityToUrl/appendIdentityToUrlOptionsValidator";

export default ({
addEcidQueryToPayload,
addQueryStringIdentityToPayload,
ensureSingleIdentity,
setLegacyEcid,
handleResponseForIdSyncs,
getEcidFromResponse,
getIdentity,
consent
consent,
appendIdentityToUrl,
logger
}) => {
let ecid;
let edge = {};
Expand All @@ -18,6 +22,7 @@ export default ({
// Querying the ECID on every request to be able to set the legacy cookie, and make it
// available for the `getIdentity` command.
addEcidQueryToPayload(request.getPayload());
addQueryStringIdentityToPayload(request.getPayload());
return ensureSingleIdentity({ request, onResponse, onRequestFailure });
},
onResponse({ response }) {
Expand Down Expand Up @@ -55,6 +60,23 @@ export default ({
};
});
}
},
appendIdentityToUrl: {
optionsValidator: appendIdentityToUrlOptionsValidator,
run: options => {
return consent
.withConsent()
.then(() => {
return ecid ? undefined : getIdentity(options.namespaces);
})
.then(() => {
return { url: appendIdentityToUrl(ecid, options.url) };
})
.catch(error => {
logger.warn(`Unable to append identity to url. ${error.message}`);
return options;
});
}
}
}
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/Identity/createLegacyIdentity.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default ({ config, getEcidFromVisitor }) => {
return Promise.resolve();
},
setEcid(ecid) {
if (idMigrationEnabled && !cookieJar.get(amcvCookieName)) {
if (idMigrationEnabled && getEcidFromLegacyCookies() !== ecid) {
cookieJar.set(amcvCookieName, `MCMID|${ecid}`, {
domain: apexDomain,
// Without `expires` this will be a session cookie.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ governing permissions and limitations under the License.

import {
createAddIdentity,
createHasIdentity,
createRequestPayload
} from "../../../utils/request";

Expand All @@ -25,6 +26,7 @@ export default namespaces => {
};
return createRequestPayload({
content,
addIdentity: createAddIdentity(content)
addIdentity: createAddIdentity(content),
hasIdentity: createHasIdentity(content)
});
};
24 changes: 21 additions & 3 deletions src/components/Identity/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ import injectEnsureSingleIdentity from "./injectEnsureSingleIdentity";
import addEcidQueryToPayload from "./addEcidQueryToPayload";
import injectSetDomainForInitialIdentityPayload from "./injectSetDomainForInitialIdentityPayload";
import injectAddLegacyEcidToPayload from "./injectAddLegacyEcidToPayload";
import injectAddQueryStringIdentityToPayload from "./injectAddQueryStringIdentityToPayload";
import addEcidToPayload from "./addEcidToPayload";
import injectAwaitIdentityCookie from "./injectAwaitIdentityCookie";
import getEcidFromResponse from "./getEcidFromResponse";
import createGetIdentity from "./getIdentity/createGetIdentity";
import createIdentityRequest from "./getIdentity/createIdentityRequest";
import createIdentityRequestPayload from "./getIdentity/createIdentityRequestPayload";
import injectAppendIdentityToUrl from "./appendIdentityToUrl/injectAppendIdentityToUrl";

const createIdentity = ({
config,
Expand All @@ -40,7 +42,7 @@ const createIdentity = ({
fireReferrerHideableImage,
sendEdgeNetworkRequest
}) => {
const { orgId, thirdPartyCookiesEnabled } = config;
const { orgId, thirdPartyCookiesEnabled, idOverwriteEnabled } = config;

const getEcidFromVisitor = injectGetEcidFromVisitor({
logger,
Expand All @@ -67,6 +69,15 @@ const createIdentity = ({
getLegacyEcid: legacyIdentity.getEcid,
addEcidToPayload
});
const addQueryStringIdentityToPayload = injectAddQueryStringIdentityToPayload(
{
locationSearch: window.document.location.search,
dateProvider: () => new Date(),
orgId,
logger,
idOverwriteEnabled
}
);
const awaitIdentityCookie = injectAwaitIdentityCookie({
doesIdentityCookieExist,
orgId
Expand All @@ -85,14 +96,21 @@ const createIdentity = ({
const handleResponseForIdSyncs = injectHandleResponseForIdSyncs({
processIdSyncs
});
const appendIdentityToUrl = injectAppendIdentityToUrl({
dateProvider: () => new Date(),
orgId
});
return createComponent({
ensureSingleIdentity,
addEcidQueryToPayload,
addQueryStringIdentityToPayload,
ensureSingleIdentity,
setLegacyEcid: legacyIdentity.setEcid,
handleResponseForIdSyncs,
getEcidFromResponse,
getIdentity,
consent
consent,
appendIdentityToUrl,
logger
});
};

Expand Down
7 changes: 7 additions & 0 deletions src/components/Identity/injectAddLegacyEcidToPayload.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@ OF ANY KIND, either express or implied. See the License for the specific languag
governing permissions and limitations under the License.
*/

import ecidNamespace from "../../constants/ecidNamespace";

export default ({ getLegacyEcid, addEcidToPayload }) => {
return payload => {
if (payload.hasIdentity(ecidNamespace)) {
// don't get the legacy identity if we already have the query string identity or if
// the user specified it in the identity map
return Promise.resolve();
}
return getLegacyEcid().then(ecidToMigrate => {
if (ecidToMigrate) {
addEcidToPayload(payload, ecidToMigrate);
Expand Down
61 changes: 61 additions & 0 deletions src/components/Identity/injectAddQueryStringIdentityToPayload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Example: adobe_mc=TS%3D1641432103%7CMCMID%3D77094828402023918047117570965393734545%7CMCORGID%3DFAF554945B90342F0A495E2C%40AdobeOrg
// Decoded: adobe_mc=TS=1641432103|MCMID=77094828402023918047117570965393734545|MCORGID=FAF554945B90342F0A495E2C@AdobeOrg

import { queryString } from "../../utils";
import queryStringIdentityParam from "../../constants/queryStringIdentityParam";
import ecidNamespace from "../../constants/ecidNamespace";

const LINK_TTL_SECONDS = 300; // 5 minute link time to live

export default ({
locationSearch,
dateProvider,
orgId,
logger,
idOverwriteEnabled
}) => payload => {
if (payload.hasIdentity(ecidNamespace)) {
// don't overwrite a user provided ecid identity
return;
}

const parsedQueryString = queryString.parse(locationSearch);
const queryStringValue = parsedQueryString[queryStringIdentityParam];
if (queryStringValue === undefined) {
return;
}

const properties = queryStringValue.split("|").reduce((memo, keyValue) => {
const [key, value] = keyValue.split("=");
memo[key] = value;
return memo;
}, {});
// We are using MCMID and MCORGID to be compatible with Visitor.
const ts = parseInt(properties.TS, 10);
const mcmid = properties.MCMID;
const mcorgid = properties.MCORGID;

if (
// When TS is not specified or not a number, the following inequality returns false.
// All inequalities with NaN variables are false.
dateProvider().getTime() / 1000 <= ts + LINK_TTL_SECONDS &&
mcorgid === orgId &&
mcmid
) {
logger.info(
`Found valid ECID identity ${mcmid} from the adobe_mc query string parameter.`
);
payload.addIdentity(ecidNamespace, {
id: mcmid
});
if (idOverwriteEnabled) {
payload.mergeQuery({
identity: {
overwriteExisting: true
}
});
}
} else {
logger.info("Detected invalid or expired adobe_mc query string parameter.");
}
};
6 changes: 6 additions & 0 deletions src/components/Privacy/createConsentRequestPayload.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export default () => {
content.identityMap[namespaceCode] =
content.identityMap[namespaceCode] || [];
content.identityMap[namespaceCode].push(identity);
},
hasIdentity: namespaceCode => {
return (
(content.identityMap && content.identityMap[namespaceCode]) !==
undefined
);
}
});

Expand Down
Loading