Skip to content

Commit

Permalink
Signal improvements (#1178)
Browse files Browse the repository at this point in the history
  • Loading branch information
silesky authored Oct 31, 2024
1 parent 11a943e commit 08e4553
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 26 deletions.
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

0 comments on commit 08e4553

Please sign in to comment.