From ccce368db98663e78eea5fff963fd62a706aea45 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Laroche Date: Mon, 24 Nov 2025 12:55:28 -0500 Subject: [PATCH 1/8] Remove lowercase for tryIdentifyFromParams --- lib/addons/try-identify.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/addons/try-identify.ts b/lib/addons/try-identify.ts index 3d097293..8e4262fb 100644 --- a/lib/addons/try-identify.ts +++ b/lib/addons/try-identify.ts @@ -26,5 +26,5 @@ OptableSDK.prototype.tryIdentifyFromParams = function (key?: string, prefix?: st return; } - this.identify((prefix || "e") + ":" + eid.toLowerCase()); + this.identify((prefix || "e") + ":" + eid); }; From 89679dc97243c591db0f790dd614ee5d3b9dc074 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Laroche Date: Mon, 24 Nov 2025 12:55:40 -0500 Subject: [PATCH 2/8] Add support for ID and neighbors for profile calls --- lib/edge/profile.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/edge/profile.ts b/lib/edge/profile.ts index 68401ca5..a9c421a3 100644 --- a/lib/edge/profile.ts +++ b/lib/edge/profile.ts @@ -5,9 +5,15 @@ type ProfileTraits = { [key: string]: string | number | boolean; }; -function Profile(config: ResolvedConfig, traits: ProfileTraits): Promise { - const profile = { +function Profile(config: ResolvedConfig, traits: ProfileTraits, id: string | null = null, neighbors: string[] | null = null): Promise { + const profile: { + traits: ProfileTraits; + id?: string | null; + neighbors?: string[] | null; + } = { traits: traits, + id: id, + neighbors: neighbors, }; return fetch("/profile", config, { From f4e42b3b484aea9cf5f821cb18c36550631a93a0 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Laroche Date: Mon, 24 Nov 2025 15:02:58 -0500 Subject: [PATCH 3/8] Fix try-identify tests --- lib/addons/try-identify.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/addons/try-identify.test.js b/lib/addons/try-identify.test.js index c09df52c..9b4941a4 100644 --- a/lib/addons/try-identify.test.js +++ b/lib/addons/try-identify.test.js @@ -25,7 +25,7 @@ describe("tryIdentifyFromParams", () => { const expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"; expect(SDK.identify.mock.calls.length).toBe(1); - expect(SDK.identify.mock.calls[0][0]).toEqual(expected); + expect(SDK.identify.mock.calls[0][0].toLowerCase()).toEqual(expected); }); test("doesnt call identify when default oeid param is absent", () => { @@ -52,7 +52,7 @@ describe("tryIdentifyFromParams", () => { const expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"; expect(SDK.identify.mock.calls.length).toBe(1); - expect(SDK.identify.mock.calls[0][0]).toEqual(expected); + expect(SDK.identify.mock.calls[0][0].toLowerCase()).toEqual(expected); }); test("matches custom param regardless of its case", () => { @@ -63,7 +63,7 @@ describe("tryIdentifyFromParams", () => { const expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"; expect(SDK.identify.mock.calls.length).toBe(1); - expect(SDK.identify.mock.calls[0][0]).toEqual(expected); + expect(SDK.identify.mock.calls[0][0].toLowerCase()).toEqual(expected); }); test("doesnt call identify when custom param is absent", () => { From d73bfd320f25dba0a70fc45833980fe4561eee1e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Laroche Date: Mon, 24 Nov 2025 15:03:50 -0500 Subject: [PATCH 4/8] Fix profile to not pass id or neighbors if not provided --- lib/edge/profile.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/edge/profile.ts b/lib/edge/profile.ts index a9c421a3..28c0962a 100644 --- a/lib/edge/profile.ts +++ b/lib/edge/profile.ts @@ -8,12 +8,12 @@ type ProfileTraits = { function Profile(config: ResolvedConfig, traits: ProfileTraits, id: string | null = null, neighbors: string[] | null = null): Promise { const profile: { traits: ProfileTraits; - id?: string | null; - neighbors?: string[] | null; + id?: string; + neighbors?: string[]; } = { traits: traits, - id: id, - neighbors: neighbors, + ...(id && { id: id }), + ...(neighbors && { neighbors: neighbors }), }; return fetch("/profile", config, { From f1f8969a9239881ba19a495b16e92f1495bc5b01 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Laroche Date: Mon, 24 Nov 2025 15:38:56 -0500 Subject: [PATCH 5/8] Enable profile call to get ID and neighbors --- lib/sdk.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/sdk.ts b/lib/sdk.ts index 35169ed4..098238ed 100644 --- a/lib/sdk.ts +++ b/lib/sdk.ts @@ -102,9 +102,9 @@ class OptableSDK { return Witness(this.dcn, event, properties); } - async profile(traits: ProfileTraits): Promise { + async profile(traits: ProfileTraits, id: string | null = null, neighbors: string[] | null = null): Promise { await this.init; - return Profile(this.dcn, traits); + return Profile(this.dcn, traits, id, neighbors); } async tokenize(id: string): Promise { From 519ffbc6df6ec1a5b87c25a04627ce195253048d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Laroche Date: Mon, 24 Nov 2025 16:00:11 -0500 Subject: [PATCH 6/8] Linting --- lib/edge/profile.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/edge/profile.ts b/lib/edge/profile.ts index 28c0962a..73d78fdb 100644 --- a/lib/edge/profile.ts +++ b/lib/edge/profile.ts @@ -5,7 +5,12 @@ type ProfileTraits = { [key: string]: string | number | boolean; }; -function Profile(config: ResolvedConfig, traits: ProfileTraits, id: string | null = null, neighbors: string[] | null = null): Promise { +function Profile( + config: ResolvedConfig, + traits: ProfileTraits, + id: string | null = null, + neighbors: string[] | null = null +): Promise { const profile: { traits: ProfileTraits; id?: string; From ebf75786c5ed2484599029c0605f174e1dd31354 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Laroche Date: Wed, 26 Nov 2025 10:07:00 -0500 Subject: [PATCH 7/8] package lock update --- package-lock.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 8d43adb6..6472c001 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,6 +80,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -2266,6 +2267,7 @@ "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.20.0" } @@ -2880,6 +2882,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -7778,6 +7781,7 @@ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7971,6 +7975,7 @@ "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -8018,6 +8023,7 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -8361,6 +8367,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dev": true, + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -10291,6 +10298,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "dev": true, + "peer": true, "requires": { "undici-types": "~6.20.0" } @@ -10763,6 +10771,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -14494,7 +14503,8 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true + "dev": true, + "peer": true }, "unbzip2-stream": { "version": "1.4.3", @@ -14631,6 +14641,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, + "peer": true, "requires": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -14675,6 +14686,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, + "peer": true, "requires": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", From ab6c35942204cdc09af4abe52e43ec8c3d476f8a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Laroche Date: Thu, 27 Nov 2025 11:11:52 -0500 Subject: [PATCH 8/8] Update README --- README.md | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3d7f3deb..ca155017 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ If you're building a web application or want to bundle the SDK functionality wit ```shell # latest stable release: -$ npm install @optable/web-sdk +npm install @optable/web-sdk ``` And then simply `import` and use the `OptableSDK` class as shown in the _Usage_ section below. @@ -92,7 +92,7 @@ const sdk = new OptableSDK({ host: "dcn.customer.com", site: "my-site", cookies: Note that the default is `cookies: true` and will be inferred if you do not specify the `cookies` parameter at all. -# Using the npm module +## Using the npm module ## Initialization Configuration (`InitConfig`) @@ -125,6 +125,7 @@ When creating an instance of `OptableSDK`, you can pass an `InitConfig` object t - **`readOnly` (boolean, default: `false`)** When set to `true`, puts the SDK in a read-only mode, preventing any data modifications while still allowing API queries. + - **`optableCacheTargeting` (string, defaults: `optable-cache:targeting`)** Local storage cache key used to store latest targeting response. @@ -201,6 +202,28 @@ type ProfileTraits = { }; ``` +You can also override the main identifier (replacing the Optable Visitor ID) as the second argument of the function. +The third argument is to provide additional identifier(s) that you want to associate to that profile. + +```javascript +const onSuccess = () => console.log("Profile API success!"); +const onFailure = (err) => console.warn("Profile API error: ${err.message}"); + +const visitorTraits = { + gender: "M", + age: 44, + favColor: "blue", + hasAccount: true, +}; + +const emailID = OptableSDK.eid("some.email@address.com"); +const additionalIDs = []; +additionalIDs.push(OptableSDK.cid("id1")); +additionalIDs.push(OptableSDK.cid("id2", "c2")); + +sdk.profile(visitorTraits, emailID, additionalIDs).then(onSuccess).catch(onFailure); +``` + ### Targeting API To get the targeting information associated by the configured DCN with the user's browser in real-time, you can call the targeting API as follows: @@ -774,7 +797,7 @@ If you send Email newsletters that contain links to your website, then you may w To enable automatic identification of visitors originating from your Email newsletter, you first need to include an **oeid** parameter in the query string of all links to your website in your Email newsletter template. The value of the **oeid** parameter should be set to the SHA256 hash of the lowercased Email address of the recipient. For example, if you are using [Braze](https://www.braze.com/) to send your newsletters, you can easily encode the SHA256 hash value of the recipient's Email address by setting the **oeid** parameter in the query string of any links to your website as follows: -``` +```javascript oeid={{${email_address} | downcase | sha2}} ``` @@ -833,16 +856,16 @@ It is recommended to call this method before making ad calls to ensure that the The demo pages are working examples of both `identify` and `targeting` APIs, as well as an integration with the [Google Ad Manager 360](https://admanager.google.com/home/) ad server, enabling the targeting of ads served by GAM360 to audiences activated in the [Optable](https://optable.co/) DCN. -You can browse a recent (but not necessarily the latest) released version of the demo pages at [https://demo.optable.co/](https://demo.optable.co/). The source code to the demos can be found [here](https://github.com/Optable/optable-web-sdk/tree/master/demos). The demo pages will connect to the [Optable](https://optable.co/) demo DCN at `sandbox.optable.co` and reference the web site slug `web-sdk-demo`. The GAM360 targeting demo loads ads from a GAM360 account operated by [Optable](https://optable.co/). +You can browse a recent (but not necessarily the latest) released version of the demo pages at [https://demo.optable.co/](https://demo.optable.co/). The source code to the demos can be found in the [demos directory](https://github.com/Optable/optable-web-sdk/tree/master/demos). The demo pages will connect to the [Optable](https://optable.co/) demo DCN at `sandbox.optable.co` and reference the web site slug `web-sdk-demo`. The GAM360 targeting demo loads ads from a GAM360 account operated by [Optable](https://optable.co/). -Note that the demo pages at [https://demo.optable.co/](https://demo.optable.co/) will by default rely on secure HTTP first-party cookies as described [here](https://github.com/Optable/optable-web-sdk#domains-and-cookies). To see an example based on [LocalStorage](https://github.com/Optable/optable-web-sdk#localstorage), see the [index-nocookies variant here](https://demo.optable.co/index-nocookies.html). +Note that the demo pages at [https://demo.optable.co/](https://demo.optable.co/) will by default rely on secure HTTP first-party cookies as described in [this section](https://github.com/Optable/optable-web-sdk#domains-and-cookies). To see an example based on [LocalStorage](https://github.com/Optable/optable-web-sdk#localstorage), see the [index-nocookies variant here](https://demo.optable.co/index-nocookies.html). To build and run the demos locally, you will need [Docker](https://www.docker.com/), `docker-compose` and `make`: -``` -$ cd path/to/optable-web-sdk -$ make -$ docker-compose up +```shell +cd path/to/optable-web-sdk +make +docker-compose up ``` Then head to [https://localhost:8180/](localhost:8180) to see the demo pages. You can modify the code in each demo, then run `make build` and finally refresh the demo pages to see your changes take effect. If you want to test the demos with your own DCN, make sure to update the configuration (hostname and site slug) given to the OptableSDK (see `webpack.config.js` for the react example).