@optable/web-sdk 0.45.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -50
- package/browser/dist/sdk.js +1 -1
- package/lib/dist/addons/gpt.js +3 -1
- package/lib/dist/addons/prebid/analytics.d.ts +106 -0
- package/lib/dist/addons/prebid/analytics.js +478 -0
- package/lib/dist/addons/prototypes/analytics.d.ts +1 -0
- package/lib/dist/addons/prototypes/analytics.js +17 -7
- package/lib/dist/build.json +1 -1
- package/lib/dist/config.d.ts +4 -1
- package/lib/dist/core/context.d.ts +32 -0
- package/lib/dist/core/context.js +146 -0
- package/lib/dist/core/storage.d.ts +1 -0
- package/lib/dist/core/storage.js +20 -0
- package/lib/dist/edge/targeting.js +3 -0
- package/lib/dist/edge/witness.d.ts +5 -2
- package/lib/dist/edge/witness.js +4 -1
- package/lib/dist/sdk.d.ts +10 -1
- package/lib/dist/sdk.js +37 -2
- package/package.json +9 -6
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const DEFAULT_MAX_CONTENT_LENGTH = 5000;
|
|
2
|
+
const DEFAULT_MAX_HTML_LENGTH = 50000;
|
|
3
|
+
const MAX_HEADINGS = 20;
|
|
4
|
+
function extractSemanticContent(config) {
|
|
5
|
+
var _a;
|
|
6
|
+
const semantic = {
|
|
7
|
+
title: document.title || "",
|
|
8
|
+
};
|
|
9
|
+
// Extract meta description
|
|
10
|
+
const descriptionMeta = document.querySelector('meta[name="description"]');
|
|
11
|
+
if (descriptionMeta) {
|
|
12
|
+
const content = descriptionMeta.getAttribute("content");
|
|
13
|
+
if (content) {
|
|
14
|
+
semantic.description = content;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Extract meta keywords
|
|
18
|
+
const keywordsMeta = document.querySelector('meta[name="keywords"]');
|
|
19
|
+
if (keywordsMeta) {
|
|
20
|
+
const content = keywordsMeta.getAttribute("content");
|
|
21
|
+
if (content) {
|
|
22
|
+
semantic.keywords = content
|
|
23
|
+
.split(",")
|
|
24
|
+
.map((k) => k.trim())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Extract canonical URL
|
|
29
|
+
const canonicalLink = document.querySelector('link[rel="canonical"]');
|
|
30
|
+
if (canonicalLink) {
|
|
31
|
+
const href = canonicalLink.getAttribute("href");
|
|
32
|
+
if (href) {
|
|
33
|
+
semantic.canonicalUrl = href;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Extract Open Graph tags
|
|
37
|
+
const ogTags = {};
|
|
38
|
+
document.querySelectorAll('meta[property^="og:"]').forEach((meta) => {
|
|
39
|
+
const property = meta.getAttribute("property");
|
|
40
|
+
const content = meta.getAttribute("content");
|
|
41
|
+
if (property && content) {
|
|
42
|
+
const key = property.replace("og:", "");
|
|
43
|
+
ogTags[key] = content;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
if (Object.keys(ogTags).length > 0) {
|
|
47
|
+
semantic.ogTags = ogTags;
|
|
48
|
+
}
|
|
49
|
+
// Extract headings (h1-h3, max 20)
|
|
50
|
+
const headings = [];
|
|
51
|
+
document.querySelectorAll("h1, h2, h3").forEach((heading) => {
|
|
52
|
+
var _a;
|
|
53
|
+
if (headings.length >= MAX_HEADINGS)
|
|
54
|
+
return;
|
|
55
|
+
const level = parseInt(heading.tagName.substring(1), 10);
|
|
56
|
+
const text = (_a = heading.textContent) === null || _a === void 0 ? void 0 : _a.trim();
|
|
57
|
+
if (text) {
|
|
58
|
+
headings.push({ level, text });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
if (headings.length > 0) {
|
|
62
|
+
semantic.headings = headings;
|
|
63
|
+
}
|
|
64
|
+
// Extract main content
|
|
65
|
+
const maxContentLength = (_a = config.maxContentLength) !== null && _a !== void 0 ? _a : DEFAULT_MAX_CONTENT_LENGTH;
|
|
66
|
+
const contentElement = findContentElement(config.contentSelector);
|
|
67
|
+
if (contentElement) {
|
|
68
|
+
const text = extractTextContent(contentElement);
|
|
69
|
+
if (text) {
|
|
70
|
+
semantic.content = text.substring(0, maxContentLength);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Extract JSON-LD
|
|
74
|
+
const jsonLdScripts = document.querySelectorAll('script[type="application/ld+json"]');
|
|
75
|
+
const jsonLdData = [];
|
|
76
|
+
jsonLdScripts.forEach((script) => {
|
|
77
|
+
try {
|
|
78
|
+
const data = JSON.parse(script.textContent || "");
|
|
79
|
+
if (data && typeof data === "object") {
|
|
80
|
+
jsonLdData.push(data);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (_a) {
|
|
84
|
+
// Ignore invalid JSON-LD
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
if (jsonLdData.length > 0) {
|
|
88
|
+
semantic.jsonLd = jsonLdData;
|
|
89
|
+
}
|
|
90
|
+
// Extract language
|
|
91
|
+
const lang = document.documentElement.getAttribute("lang");
|
|
92
|
+
if (lang) {
|
|
93
|
+
semantic.language = lang;
|
|
94
|
+
}
|
|
95
|
+
return semantic;
|
|
96
|
+
}
|
|
97
|
+
function findContentElement(selector) {
|
|
98
|
+
// Use provided selector if available
|
|
99
|
+
if (selector) {
|
|
100
|
+
return document.querySelector(selector);
|
|
101
|
+
}
|
|
102
|
+
// Fall back to heuristics: main, article, or first large content block
|
|
103
|
+
const candidates = ["main", "article", '[role="main"]', ".content", "#content", ".post", ".article"];
|
|
104
|
+
for (const candidate of candidates) {
|
|
105
|
+
const element = document.querySelector(candidate);
|
|
106
|
+
if (element) {
|
|
107
|
+
return element;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Last resort: body
|
|
111
|
+
return document.body;
|
|
112
|
+
}
|
|
113
|
+
function extractTextContent(element) {
|
|
114
|
+
if (element instanceof HTMLElement && element.innerText) {
|
|
115
|
+
return element.innerText.trim();
|
|
116
|
+
}
|
|
117
|
+
return "";
|
|
118
|
+
}
|
|
119
|
+
function extractContext(config) {
|
|
120
|
+
var _a;
|
|
121
|
+
const contextData = {
|
|
122
|
+
semantic: extractSemanticContent(config),
|
|
123
|
+
url: window.location.href,
|
|
124
|
+
extractedAt: Date.now(),
|
|
125
|
+
};
|
|
126
|
+
// Include referrer if available
|
|
127
|
+
if (document.referrer) {
|
|
128
|
+
contextData.referrer = document.referrer;
|
|
129
|
+
}
|
|
130
|
+
// Include HTML if configured
|
|
131
|
+
if (config.includeHtml) {
|
|
132
|
+
const maxHtmlLength = (_a = config.maxHtmlLength) !== null && _a !== void 0 ? _a : DEFAULT_MAX_HTML_LENGTH;
|
|
133
|
+
contextData.html = document.documentElement.outerHTML.substring(0, maxHtmlLength);
|
|
134
|
+
}
|
|
135
|
+
return contextData;
|
|
136
|
+
}
|
|
137
|
+
function normalizeContextConfig(config) {
|
|
138
|
+
if (!config) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
if (config === true) {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
return config;
|
|
145
|
+
}
|
|
146
|
+
export { extractContext, extractSemanticContent, normalizeContextConfig };
|
|
@@ -12,6 +12,7 @@ declare class LocalStorage {
|
|
|
12
12
|
constructor(config: ResolvedConfig);
|
|
13
13
|
getPassport(): string | null;
|
|
14
14
|
setPassport(passport: string): void;
|
|
15
|
+
getVisitorId(): string | null;
|
|
15
16
|
getTargeting(): TargetingResponse | null;
|
|
16
17
|
setTargeting(targeting?: TargetingResponse | null): void;
|
|
17
18
|
getSite(): SiteResponse | null;
|
package/lib/dist/core/storage.js
CHANGED
|
@@ -16,6 +16,26 @@ class LocalStorage {
|
|
|
16
16
|
setPassport(passport) {
|
|
17
17
|
this.writeToStorageKeys(this.passportKeys, passport);
|
|
18
18
|
}
|
|
19
|
+
getVisitorId() {
|
|
20
|
+
const passport = this.getPassport();
|
|
21
|
+
if (!passport)
|
|
22
|
+
return null;
|
|
23
|
+
const payload = passport.split(".")[1];
|
|
24
|
+
if (!payload)
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
// JWT payload is base64url; normalize to base64 before atob.
|
|
28
|
+
const b64 = payload
|
|
29
|
+
.replace(/-/g, "+")
|
|
30
|
+
.replace(/_/g, "/")
|
|
31
|
+
.padEnd(payload.length + ((4 - (payload.length % 4)) % 4), "=");
|
|
32
|
+
const claims = JSON.parse(atob(b64));
|
|
33
|
+
return typeof (claims === null || claims === void 0 ? void 0 : claims.id) === "string" ? claims.id : null;
|
|
34
|
+
}
|
|
35
|
+
catch (_a) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
19
39
|
getTargeting() {
|
|
20
40
|
const raw = this.readStorageKeys(this.targetingKeys);
|
|
21
41
|
return raw ? JSON.parse(raw) : null;
|
|
@@ -52,6 +52,9 @@ function Targeting(config, req) {
|
|
|
52
52
|
if (abTest.skipMatchers) {
|
|
53
53
|
searchParams.append("skip_matchers", abTest.skipMatchers.join(","));
|
|
54
54
|
}
|
|
55
|
+
if (abTest.skipResolvers) {
|
|
56
|
+
searchParams.append("skip_resolvers", abTest.skipResolvers.join(","));
|
|
57
|
+
}
|
|
55
58
|
}
|
|
56
59
|
if ((_a = config.additionalTargetingSignals) === null || _a === void 0 ? void 0 : _a.ref) {
|
|
57
60
|
searchParams.append("ref", `${window.location.protocol}//${window.location.host}${window.location.pathname}`);
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ResolvedConfig } from "../config";
|
|
2
|
+
import type { ContextData } from "../core/context";
|
|
2
3
|
type WitnessProperties = {
|
|
3
|
-
[key: string]: string | number | boolean
|
|
4
|
+
[key: string]: string | number | boolean | unknown[] | null | {
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
};
|
|
4
7
|
};
|
|
5
|
-
declare function Witness(config: ResolvedConfig, event: string, properties: WitnessProperties): Promise<void>;
|
|
8
|
+
declare function Witness(config: ResolvedConfig, event: string, properties: WitnessProperties, context?: ContextData): Promise<void>;
|
|
6
9
|
export { Witness, WitnessProperties };
|
|
7
10
|
export default Witness;
|
package/lib/dist/edge/witness.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { fetch } from "../core/network";
|
|
2
|
-
function Witness(config, event, properties) {
|
|
2
|
+
function Witness(config, event, properties, context) {
|
|
3
3
|
const evt = {
|
|
4
4
|
event: event,
|
|
5
5
|
properties: properties,
|
|
6
6
|
};
|
|
7
|
+
if (context) {
|
|
8
|
+
evt.pageContext = context;
|
|
9
|
+
}
|
|
7
10
|
return fetch("/witness", config, {
|
|
8
11
|
method: "POST",
|
|
9
12
|
headers: {
|
package/lib/dist/sdk.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ declare class OptableSDK {
|
|
|
10
10
|
static version: string;
|
|
11
11
|
dcn: ResolvedConfig;
|
|
12
12
|
protected init: Promise<void>;
|
|
13
|
+
private contextSent;
|
|
14
|
+
private contextConfig;
|
|
15
|
+
private passportNullWarned;
|
|
16
|
+
private visitorIdNullWarned;
|
|
13
17
|
constructor(dcn: InitConfig);
|
|
14
18
|
initialize(): Promise<void>;
|
|
15
19
|
identify(...ids: string[]): Promise<void>;
|
|
@@ -18,12 +22,17 @@ declare class OptableSDK {
|
|
|
18
22
|
targetingFromCache(): TargetingResponse | null;
|
|
19
23
|
site(): Promise<SiteResponse>;
|
|
20
24
|
siteFromCache(): SiteResponse | null;
|
|
25
|
+
passport(): string | null;
|
|
26
|
+
visitorId(): string | null;
|
|
21
27
|
targetingClearCache(): void;
|
|
22
28
|
prebidORTB2(): Promise<PrebidORTB2>;
|
|
23
29
|
prebidORTB2FromCache(): PrebidORTB2;
|
|
24
30
|
targetingKeyValues(): Promise<TargetingKeyValues>;
|
|
25
31
|
targetingKeyValuesFromCache(): TargetingKeyValues;
|
|
26
|
-
witness(event: string, properties?: WitnessProperties
|
|
32
|
+
witness(event: string, properties?: WitnessProperties, options?: {
|
|
33
|
+
includeContext?: boolean;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
resetContext(): void;
|
|
27
36
|
profile(traits: ProfileTraits, id?: string | null, neighbors?: string[] | null): Promise<void>;
|
|
28
37
|
tokenize(id: string): Promise<TokenizeResponse>;
|
|
29
38
|
resolve(id?: string): Promise<ResolveResponse>;
|
package/lib/dist/sdk.js
CHANGED
|
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import { default as buildInfo } from "./build.json";
|
|
11
11
|
import { getConfig } from "./config";
|
|
12
|
+
import { extractContext, normalizeContextConfig } from "./core/context";
|
|
12
13
|
import { Identify } from "./edge/identify";
|
|
13
14
|
import { Uid2Token } from "./edge/uid2_token";
|
|
14
15
|
import { Resolve } from "./edge/resolve";
|
|
@@ -19,9 +20,15 @@ import { Witness } from "./edge/witness";
|
|
|
19
20
|
import { Profile } from "./edge/profile";
|
|
20
21
|
import { sha256 } from "js-sha256";
|
|
21
22
|
import { Tokenize } from "./edge/tokenize";
|
|
23
|
+
import { LocalStorage } from "./core/storage";
|
|
22
24
|
class OptableSDK {
|
|
23
25
|
constructor(dcn) {
|
|
26
|
+
this.contextSent = false;
|
|
27
|
+
this.contextConfig = null;
|
|
28
|
+
this.passportNullWarned = false;
|
|
29
|
+
this.visitorIdNullWarned = false;
|
|
24
30
|
this.dcn = getConfig(dcn);
|
|
31
|
+
this.contextConfig = normalizeContextConfig(dcn.pageContext);
|
|
25
32
|
this.init = this.initialize();
|
|
26
33
|
}
|
|
27
34
|
initialize() {
|
|
@@ -64,6 +71,26 @@ class OptableSDK {
|
|
|
64
71
|
siteFromCache() {
|
|
65
72
|
return SiteFromCache(this.dcn);
|
|
66
73
|
}
|
|
74
|
+
passport() {
|
|
75
|
+
const value = new LocalStorage(this.dcn).getPassport();
|
|
76
|
+
if (value === null && !this.passportNullWarned) {
|
|
77
|
+
this.passportNullWarned = true;
|
|
78
|
+
console.warn("[Optable] passport() returned null. The passport is cached in localStorage once the DCN returns one. " +
|
|
79
|
+
"Call before initialization (await sdk.site() or sdk.targeting()) may return null, and deployments where the DCN " +
|
|
80
|
+
"does not echo the passport in response bodies will never populate it client-side.");
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
visitorId() {
|
|
85
|
+
const value = new LocalStorage(this.dcn).getVisitorId();
|
|
86
|
+
if (value === null && !this.visitorIdNullWarned) {
|
|
87
|
+
this.visitorIdNullWarned = true;
|
|
88
|
+
console.warn("[Optable] visitorId() returned null. The visitor ID is derived from the passport JWT in localStorage. " +
|
|
89
|
+
"Call before initialization (await sdk.site() or sdk.targeting()) may return null, and deployments where the DCN " +
|
|
90
|
+
"does not echo the passport in response bodies will never populate it client-side.");
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
67
94
|
targetingClearCache() {
|
|
68
95
|
TargetingClearCache(this.dcn);
|
|
69
96
|
}
|
|
@@ -86,11 +113,19 @@ class OptableSDK {
|
|
|
86
113
|
return TargetingKeyValues(tdata);
|
|
87
114
|
}
|
|
88
115
|
witness(event_1) {
|
|
89
|
-
return __awaiter(this, arguments, void 0, function* (event, properties = {}) {
|
|
116
|
+
return __awaiter(this, arguments, void 0, function* (event, properties = {}, options = {}) {
|
|
90
117
|
yield this.init;
|
|
91
|
-
|
|
118
|
+
let context;
|
|
119
|
+
if (options.includeContext && this.contextConfig && !this.contextSent) {
|
|
120
|
+
context = extractContext(this.contextConfig);
|
|
121
|
+
this.contextSent = true;
|
|
122
|
+
}
|
|
123
|
+
return Witness(this.dcn, event, properties, context);
|
|
92
124
|
});
|
|
93
125
|
}
|
|
126
|
+
resetContext() {
|
|
127
|
+
this.contextSent = false;
|
|
128
|
+
}
|
|
94
129
|
profile(traits_1) {
|
|
95
130
|
return __awaiter(this, arguments, void 0, function* (traits, id = null, neighbors = null) {
|
|
96
131
|
yield this.init;
|
package/package.json
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
"browser": "./lib/dist/sdk.js",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/
|
|
8
|
+
"url": "git+https://github.com/Optable/optable-web-sdk.git"
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://optable.co",
|
|
11
11
|
"license": "SEE LICENSE IN LICENSE",
|
|
12
|
-
"version": "v0.
|
|
12
|
+
"version": "v0.49.0",
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@babel/core": "^7.12.3",
|
|
15
15
|
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"@babel/preset-typescript": "^7.12.1",
|
|
19
19
|
"@types/googletag": "^3.1.3",
|
|
20
20
|
"@types/jest": "^26.0.15",
|
|
21
|
+
"@types/node": "^25.0.3",
|
|
21
22
|
"babel-jest": "^29.7.0",
|
|
22
23
|
"babel-loader": "^8.2.2",
|
|
23
24
|
"core-js": "^3.7.0",
|
|
@@ -28,13 +29,15 @@
|
|
|
28
29
|
"prettier": "^3.6.2",
|
|
29
30
|
"shellcheck": "^3.0.0",
|
|
30
31
|
"typescript": "^5.2.2",
|
|
31
|
-
"webpack": "^5.
|
|
32
|
+
"webpack": "^5.104.1",
|
|
32
33
|
"webpack-cli": "^5.1.4",
|
|
33
34
|
"whatwg-fetch": "^3.6.20"
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
36
37
|
"@babel/runtime": "^7.27.0",
|
|
37
38
|
"@optable/web-sdk": "^0.40.0",
|
|
39
|
+
"bowser": "^2.12.1",
|
|
40
|
+
"iab-adcom": "^1.0.6",
|
|
38
41
|
"iab-openrtb": "^1.0.1",
|
|
39
42
|
"js-sha256": "^0.11.0",
|
|
40
43
|
"regenerator-runtime": "^0.13.7"
|
|
@@ -47,12 +50,12 @@
|
|
|
47
50
|
"LICENSE"
|
|
48
51
|
],
|
|
49
52
|
"scripts": {
|
|
50
|
-
"build": "
|
|
53
|
+
"build": "pnpm build-lib && pnpm build-web",
|
|
51
54
|
"start": "tsc -b browser --watch & webpack --config=./browser/webpack.config.js --mode=development --watch --devtool=source-map",
|
|
52
55
|
"build-lib": "tsc -b lib",
|
|
53
56
|
"build-web": "tsc -b browser && webpack --config=./browser/webpack.config.js",
|
|
54
57
|
"test": "jest",
|
|
55
58
|
"format": "prettier . --write",
|
|
56
|
-
"lint-scripts": "
|
|
59
|
+
"lint-scripts": "pnpm exec shellcheck scripts/*.sh"
|
|
57
60
|
}
|
|
58
|
-
}
|
|
61
|
+
}
|