-
Notifications
You must be signed in to change notification settings - Fork 459
feat(api): add user agent integration attribution to PostHog events #3303
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -22,6 +22,9 @@ const ( | |||||||||||||
|
|
||||||||||||||
| infraVersionKey = "infra_version" | ||||||||||||||
| infraVersion = "v1" | ||||||||||||||
|
|
||||||||||||||
| jsSDKUserAgentPrefix = "e2b-js-sdk/" | ||||||||||||||
| pythonSDKUserAgentPrefix = "e2b-python-sdk/" | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| type PosthogClient struct { | ||||||||||||||
|
|
@@ -111,5 +114,41 @@ func (p *PosthogClient) GetPackageToPosthogProperties(header *http.Header) posth | |||||||||||||
| Set("sdk_runtime", header.Get("sdk_runtime")). | ||||||||||||||
| Set("system", header.Get("system")) | ||||||||||||||
|
|
||||||||||||||
| if userAgent := header.Get("User-Agent"); userAgent != "" { | ||||||||||||||
| properties = properties.Set("user_agent", userAgent) | ||||||||||||||
|
|
||||||||||||||
| if name, version, ok := integrationFromUserAgent(userAgent); ok { | ||||||||||||||
| properties = properties. | ||||||||||||||
| Set("integration", name). | ||||||||||||||
| Set("integration_version", version) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| return properties | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // integrationFromUserAgent extracts the integration wrapping the E2B SDK from | ||||||||||||||
| // a User-Agent like "e2b-js-sdk/1.2.3 e2b-cli/1.0.5": the first "name/version" | ||||||||||||||
| // token following an SDK token. Requiring the SDK token first prevents | ||||||||||||||
| // misreading browser User-Agents (e.g. "Mozilla/5.0 ...") as integrations. | ||||||||||||||
| func integrationFromUserAgent(userAgent string) (name, version string, ok bool) { | ||||||||||||||
| sawSDK := false | ||||||||||||||
|
|
||||||||||||||
| for token := range strings.FieldsSeq(userAgent) { | ||||||||||||||
| if strings.HasPrefix(token, jsSDKUserAgentPrefix) || strings.HasPrefix(token, pythonSDKUserAgentPrefix) { | ||||||||||||||
| sawSDK = true | ||||||||||||||
|
|
||||||||||||||
| continue | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if !sawSDK { | ||||||||||||||
| continue | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if name, version, found := strings.Cut(token, "/"); found && name != "" && version != "" { | ||||||||||||||
| return name, version, true | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+148
to
+150
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PR description mentions that the CLI's To make this behavior robust and independent of token ordering, we should explicitly exclude
Suggested change
|
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| return "", "", false | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| package analyticscollector | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestIntegrationFromUserAgent(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| userAgent string | ||
| wantName string | ||
| wantVersion string | ||
| wantOK bool | ||
| }{ | ||
| { | ||
| name: "CLI via JS SDK", | ||
| userAgent: "e2b-js-sdk/1.2.3 e2b-cli/1.0.5", | ||
| wantName: "e2b-cli", | ||
| wantVersion: "1.0.5", | ||
| wantOK: true, | ||
| }, | ||
| { | ||
| name: "CLI with command attribution picks the integration", | ||
| userAgent: "e2b-js-sdk/1.2.3 e2b-cli/1.0.5 e2b-cli-command/sandbox.list", | ||
| wantName: "e2b-cli", | ||
| wantVersion: "1.0.5", | ||
| wantOK: true, | ||
| }, | ||
| { | ||
| name: "code interpreter via Python SDK", | ||
| userAgent: "e2b-python-sdk/2.0.0 e2b-code-interpreter/0.1.0", | ||
| wantName: "e2b-code-interpreter", | ||
| wantVersion: "0.1.0", | ||
| wantOK: true, | ||
| }, | ||
| { | ||
| name: "plain SDK without integration", | ||
| userAgent: "e2b-js-sdk/1.2.3", | ||
| wantOK: false, | ||
| }, | ||
| { | ||
| name: "browser user agent is not an integration", | ||
| userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", | ||
| wantOK: false, | ||
| }, | ||
| { | ||
| name: "empty user agent", | ||
| userAgent: "", | ||
| wantOK: false, | ||
| }, | ||
| { | ||
| name: "token without version is skipped", | ||
| userAgent: "e2b-js-sdk/1.2.3 e2b-cli/", | ||
| wantOK: false, | ||
| }, | ||
|
Comment on lines
+56
to
+60
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To verify that {
name: "token without version is skipped",
userAgent: "e2b-js-sdk/1.2.3 e2b-cli/",
wantOK: false,
},
{
name: "cli command token is ignored even if first",
userAgent: "e2b-js-sdk/1.2.3 e2b-cli-command/sandbox.list",
wantOK: false,
}, |
||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| name, version, ok := integrationFromUserAgent(tt.userAgent) | ||
| assert.Equal(t, tt.wantOK, ok) | ||
| assert.Equal(t, tt.wantName, name) | ||
| assert.Equal(t, tt.wantVersion, version) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestGetPackageToPosthogPropertiesUserAgent(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| p := &PosthogClient{} | ||
|
|
||
| t.Run("integration traffic", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| header := http.Header{} | ||
| header.Set("User-Agent", "e2b-js-sdk/1.2.3 e2b-cli/1.0.5") | ||
|
|
||
| properties := p.GetPackageToPosthogProperties(&header) | ||
| assert.Equal(t, "e2b-js-sdk/1.2.3 e2b-cli/1.0.5", properties["user_agent"]) | ||
| assert.Equal(t, "e2b-cli", properties["integration"]) | ||
| assert.Equal(t, "1.0.5", properties["integration_version"]) | ||
| }) | ||
|
|
||
| t.Run("no user agent", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| header := http.Header{} | ||
|
|
||
| properties := p.GetPackageToPosthogProperties(&header) | ||
| assert.NotContains(t, properties, "user_agent") | ||
| assert.NotContains(t, properties, "integration") | ||
| assert.NotContains(t, properties, "integration_version") | ||
| }) | ||
|
|
||
| t.Run("plain SDK traffic has no integration", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| header := http.Header{} | ||
| header.Set("User-Agent", "e2b-python-sdk/2.0.0") | ||
|
|
||
| properties := p.GetPackageToPosthogProperties(&header) | ||
| assert.Equal(t, "e2b-python-sdk/2.0.0", properties["user_agent"]) | ||
| assert.NotContains(t, properties, "integration") | ||
| assert.NotContains(t, properties, "integration_version") | ||
| }) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
strings.FieldsSeqintroduces a dependency on Go 1.24 (which added iterators to thestringspackage). If the project or its deployment environments are using Go 1.23 or earlier, this will cause a compilation error.Using
strings.Fieldsis fully backward-compatible and has negligible overhead for typical short User-Agent strings.