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

Signal improvements #1178

Merged
merged 2 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/wise-coats-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@segment/analytics-signals': patch
---

* Refactor disallowList logic to never allow api.segment.io
* Update README
* Export SignalsPlugin in umd as well as global
18 changes: 8 additions & 10 deletions packages/signals/signals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,20 @@ See: [settings.ts](src/types/settings.ts)
<head>
<title>My Website</title>

<!-- Load SignalsPlugin -->
<script src="https://cdn.jsdelivr.net/npm/@segment/analytics-signals@latest/dist/umd/analytics-signals.umd.js"></script>

<!-- Load Segment (copy snippet from app.segment.com) -->
<!-- Load Segment (find and replace 'MY_WRITEKEY') -->
<script>
!function(){var i="analytics",analytics=window[i]... // etc
analytics.load("<YOUR_WRITE_KEY>");
!function(){var i="analytics",analytics=window[i]=window[i]||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","screen","once","off","on","addSourceMiddleware","addIntegrationMiddleware","setAnonymousId","addDestinationMiddleware","register"];analytics.factory=function(e){return function(){if(window[i].initialized)return window[i][e].apply(window[i],arguments);var n=Array.prototype.slice.call(arguments);if(["track","screen","alias","group","page","identify"].indexOf(e)>-1){var c=document.querySelector("link[rel='canonical']");n.push({__t:"bpc",c:c&&c.getAttribute("href")||void 0,p:location.pathname,u:location.href,s:location.search,t:document.title,r:document.referrer})}n.unshift(e);analytics.push(n);return analytics}};for(var n=0;n<analytics.methods.length;n++){var key=analytics.methods[n];analytics[key]=analytics.factory(key)}analytics.load=function(key,n){var t=document.createElement("script");t.type="text/javascript";t.async=!0;t.setAttribute("data-global-segment-analytics-key",i);t.src="https://cdn.segment.com/analytics.js/v1/" + key + "/analytics.min.js";var r=document.getElementsByTagName("script")[0];r.parentNode.insertBefore(t,r);analytics._loadOptions=n};analytics._writeKey="MY_WRITEKEY";;analytics.SNIPPET_VERSION="5.2.0";
analytics.page();
}()
}}();
</script>

<!-- Register SignalsPlugin -->
<!-- Register SignalsPlugin -->
<script src="https://cdn.jsdelivr.net/npm/@segment/analytics-signals@latest/dist/umd/analytics-signals.umd.js"></script>
<script>
const signalsPlugin = new SignalsPlugin()
var signalsPlugin = new SignalsPlugin()
analytics.register(signalsPlugin)
analytics.load(analytics._writeKey)
</script>

</head>
```

Expand Down
3 changes: 1 addition & 2 deletions packages/signals/signals/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@
"build:esm": "yarn tsc -p tsconfig.build.json",
"build:cjs": "yarn tsc -p tsconfig.build.json --outDir ./dist/cjs --module commonjs",
"build:bundle": "NODE_ENV=production yarn run webpack",
"build:bundle-dev": "NODE_ENV=development yarn run webpack",
"workerbox": "node scripts/build-workerbox.js",
"assert-workerbox-built": "sh scripts/assert-workerbox-built.sh",
"watch": "yarn concurrently 'yarn build:bundle-dev --watch' 'yarn build:esm --watch'",
"watch": "yarn concurrently 'yarn build:bundle --watch' 'yarn build:esm --watch'",
"version": "sh scripts/version.sh",
"watch:test": "yarn test --watch",
"tsc": "yarn run -T tsc",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,24 @@ describe(NetworkGenerator, () => {
expect(mockEmitter.emit.mock.calls).toEqual([])
unregister()
})

it('always disallows segment api network signals', async () => {
const mockEmitter = { emit: jest.fn() }
const networkGenerator = new TestNetworkGenerator({
networkSignalsAllowList: ['.*'],
})
const unregister = networkGenerator.register(
mockEmitter as unknown as SignalEmitter
)

await window.fetch(`https://api.segment.io`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ key: 'value' }),
})

await sleep(100)
expect(mockEmitter.emit.mock.calls).toEqual([])
unregister()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,7 @@ export const containsContentType = (
return false
}
const normalizedHeaders = normalizeHeaders(headers)

// format the content-type header to remove charset -- this is non-standard behavior that is somewhat common
// e.g. application/json;charset=utf-8 => application/json
const removeCharset = (header: string | null): string | null =>
header?.split(';')[0].trim() ?? null

return match.some((t) =>
removeCharset(normalizedHeaders.get('content-type'))?.includes(t)
)
return match.some((t) => normalizedHeaders.get('content-type')?.includes(t))
}

export const containsJSONContentType = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,24 @@ class NetworkFilterListItem {
export class NetworkSignalsFilterList {
public allowed: NetworkFilterListItem
public disallowed: NetworkFilterListItem
private disallowedDefaults: NetworkFilterListItem

constructor(
allowList: RegexLike[] | undefined,
disallowList: RegexLike[] | undefined
) {
this.allowed = new NetworkFilterListItem(allowList || [])
this.disallowed = new NetworkFilterListItem(disallowList || [])
this.disallowedDefaults = new NetworkFilterListItem([
'api.segment.io',
'signals.segment.io',
'cdn.segment.com',
])
}

isAllowed(url: string): boolean {
const disallowed = this.disallowed.test(url)
const disallowed =
this.disallowed.test(url) || this.disallowedDefaults.test(url)
const allowed = this.allowed.test(url)
return allowed && !disallowed
}
Expand All @@ -85,6 +92,7 @@ export class NetworkSignalsFilter {
isAllowed(url: string): boolean {
const { networkSignalsFilterList, networkSignalsAllowSameDomain } =
this.settings

const passesNetworkFilter = networkSignalsFilterList.isAllowed(url)
const allowedBecauseSameDomain =
networkSignalsAllowSameDomain && isSameDomain(url)
Expand Down
3 changes: 0 additions & 3 deletions packages/signals/signals/src/core/signals/signals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,6 @@ export class Signals implements ISignals {
disallowListURLs: [
analyticsService.instance.settings.apiHost,
analyticsService.instance.settings.cdnURL,
'api.segment.io',
'signals.segment.io',
'cdn.segment.com',
],
sampleRate:
analyticsService.instance.settings.cdnSettings
Expand Down
6 changes: 6 additions & 0 deletions packages/signals/signals/src/index.umd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@
*/
import { SignalsPlugin } from './index'
export { SignalsPlugin } // in case someone wants to use the umd module directly

// this will almost certainly be executed in the browser, but since this is UMD,
// we are checking just for the sake of being thorough
if (typeof window !== 'undefined') {
;(window as any).SignalsPlugin = SignalsPlugin
}
3 changes: 2 additions & 1 deletion playgrounds/standalone-playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
},
"dependencies": {
"@segment/analytics-consent-wrapper-onetrust": "workspace:^",
"@segment/analytics-next": "workspace:^"
"@segment/analytics-next": "workspace:^",
"@segment/analytics-signals": "workspace:^"
},
"devDependencies": {
"http-server": "14.1.1"
Expand Down
126 changes: 126 additions & 0 deletions playgrounds/standalone-playground/pages/index-signals.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<html>

<head>
<style>
body {
font-family: monospace;
}

#event {
margin: 2em 0;
min-height: 200px;
min-width: 700px;
}
</style>

<form method="get">
<input type="text" name="writeKey" placeholder="Writekey" />
<button>Load</button>
</form>
</script>
<script>
const { searchParams } = new URL(document.location);
const writeKey = searchParams.get("writeKey");
document.querySelector("input").value = writeKey;

if (writeKey) {
console.profile('snippet')
console.time('snippet')
!function () {
var i = "analytics", analytics = window[i] = window[i] || []; if (!analytics.initialize) if (analytics.invoked) window.console && console.error && console.error("Segment snippet included twice."); else {
analytics.invoked = !0; analytics.methods = ["trackSubmit", "trackClick", "trackLink", "trackForm", "pageview", "identify", "reset", "group", "track", "ready", "alias", "debug", "page", "screen", "once", "off", "on", "addSourceMiddleware", "addIntegrationMiddleware", "setAnonymousId", "addDestinationMiddleware", "register"]; analytics.factory = function (e) { return function () { if (window[i].initialized) return window[i][e].apply(window[i], arguments); var n = Array.prototype.slice.call(arguments); if (["track", "screen", "alias", "group", "page", "identify"].indexOf(e) > -1) { var c = document.querySelector("link[rel='canonical']"); n.push({ __t: "bpc", c: c && c.getAttribute("href") || void 0, p: location.pathname, u: location.href, s: location.search, t: document.title, r: document.referrer }) } n.unshift(e); analytics.push(n); return analytics } }; for (var n = 0; n < analytics.methods.length; n++) { var key = analytics.methods[n]; analytics[key] = analytics.factory(key) } analytics.load = function (key, n) { var t = document.createElement("script"); t.type = "text/javascript"; t.async = !0; t.setAttribute("data-global-segment-analytics-key", i); t.src = "https://cdn.segment.com/analytics.js/v1/" + key + "/analytics.min.js"; var r = document.getElementsByTagName("script")[0]; r.parentNode.insertBefore(t, r); analytics._loadOptions = n }; analytics._writeKey = writeKey;; analytics.SNIPPET_VERSION = "5.2.0";
analytics.page();
}
}();
}
</script>
<script src="/node_modules/@segment/analytics-signals/dist/umd/analytics-signals.umd.js"></script>
<script>
var signalsPlugin = new SignalsPlugin()
analytics.register(signalsPlugin)
analytics.load(analytics._writeKey)
</script>

<body>
<form>
<textarea name="event" id="event">
{
"name": "hi",
"properties": { },
"traits": { },
"options": { }
}
</textarea>
<div>
<button id="track">Track</button>
<button id="identify">Identify</button>
</div>
</form>

<pre id="ready-logs"></pre>
<pre id="logs"></pre>

<script type="text/javascript">
if (window.analytics) {
window.analytics.ready(function onReady() {
console.profileEnd('snippet')
console.timeEnd('snippet')
document.querySelector('#ready-logs').textContent = 'ready!'
})

document.querySelector('#track').addEventListener('click', function (e) {
e.preventDefault()
var contents = document.querySelector('#event').value
var evt = JSON.parse(contents)
console.profile('track')
console.time('track')
var promise = window.analytics.track(
evt.name || '',
evt.properties || {},
evt.options || {}
)

promise &&
promise.then &&
promise.then(function (ctx) {
console.timeEnd('track')
console.profileEnd('track')
ctx.flush()
document.querySelector('#logs').textContent = JSON.stringify(
ctx.event,
null,
' '
)
})
})

document
.querySelector('#identify')
.addEventListener('click', function (e) {
e.preventDefault()
var contents = document.querySelector('#event').value
var evt = JSON.parse(contents)
console.time('identify')
var promise = window.analytics.identify(
evt.name || '',
evt.properties || {},
evt.options || {}
)

promise &&
promise.then &&
promise.then(function (ctx) {
console.timeEnd('identify')
ctx.flush()
document.querySelector('#logs').textContent = JSON.stringify(
ctx.event,
null,
' '
)
})
})
}
</script>
</body>

</html>
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3948,6 +3948,7 @@ __metadata:
dependencies:
"@segment/analytics-consent-wrapper-onetrust": "workspace:^"
"@segment/analytics-next": "workspace:^"
"@segment/analytics-signals": "workspace:^"
http-server: 14.1.1
languageName: unknown
linkType: soft
Expand Down
Loading