Skip to content
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
.idea
tool/test.sqlite3
metadata.sqlite3.new
tool/ingest
38 changes: 38 additions & 0 deletions api-js/src/SDKMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import sdkNames from './data/names.json'
import sdkTypes from './data/types.json'
import sdkPopularity from './data/popularity.json'
import sdkReleases from './data/releases.json'
import sdkUserAgents from './data/user_agents.json'

export enum Type {
// ClientSide is an SDK that runs in a client scenario.
Expand Down Expand Up @@ -68,3 +69,40 @@ export namespace ReleaseHelpers {
export const Earliest = (releases: Release[]) => releases[releases.length - 1];
export const Latest = (releases: Release[]) => releases[0];
}

export interface UserAgent {
userAgents?: string[];
wrapperNames?: string[];
}

export const UserAgents: Record<string, UserAgent> = sdkUserAgents;

export namespace UserAgentHelpers {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a somewhat strange pattern, but it is what was done in this package already. Probably could just be top level functions.

/**
* Attempts to find an SDK name by checking wrapper names and user agents.
* First checks wrapper names, then user agents, in alphabetical order by SDK ID.
*
* @param identifier - The wrapper name or user agent string to search for
* @returns The SDK name if found, undefined if not found
*/
export const getSDKNameByWrapperOrUserAgent = (identifier: string): string | undefined => {
// Sort the entries by SDK ID to ensure consistent ordering.
const sortedEntries = Object.entries(UserAgents).sort(([a], [b]) => a.localeCompare(b));

// First check wrapper names
for (const [sdkId, info] of sortedEntries) {
if (info.wrapperNames?.includes(identifier)) {
return Names[sdkId];
}
}

// Then check user agents
for (const [sdkId, info] of sortedEntries) {
if (info.userAgents?.includes(identifier)) {
return Names[sdkId];
}
}

return undefined;
}
}
40 changes: 40 additions & 0 deletions api-js/src/data/user_agents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this information to js-core and then did a crawl for just these products.

"akamai-base": {
"userAgents": [
"AkamaiEdgeSDK"
]
},
"akamai-edgekv": {
"userAgents": [
"AkamaiEdgeSDK"
]
},
"cloudflare": {
"userAgents": [
"CloudflareEdgeSDK"
]
},
"fastly": {
"userAgents": [
"FastlyEdgeSDK"
]
},
"node-server": {
"userAgents": [
"NodeJSClient"
]
},
"react-native": {
"userAgents": [
"ReactNativeClient"
],
"wrapperNames": [
"react-native-client"
]
},
"vercel": {
"userAgents": [
"VercelEdgeSDK"
]
}
}
41 changes: 41 additions & 0 deletions api-js/tests/e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Names, Repos, Types, Type, Popularity, Languages, Releases, ReleaseHelpers } from '../src/SDKMeta';
import { UserAgents, UserAgentHelpers } from '../src/SDKMeta';

test('names', () => {
expect(Names['node-server']).toBe('Node.js Server SDK');
Expand Down Expand Up @@ -67,3 +68,43 @@ test('eol calculations', () => {
expect(ReleaseHelpers.IsApproachingEOL(earliest, new Date(earliestEOL.getTime() - thirty_minutes), hour)).toBe(true);
expect(ReleaseHelpers.IsApproachingEOL(earliest, new Date(earliestEOL.getTime() - minute), hour)).toBe(true);
})

test('user agents', () => {
// Test basic user agent data structure
expect(UserAgents['node-server']).toBeDefined();
expect(UserAgents['node-server'].userAgents).toContain('NodeJSClient');

// Test SDK with both user agents and wrapper names
expect(UserAgents['react-native']).toBeDefined();
expect(UserAgents['react-native'].userAgents).toContain('ReactNativeClient');
expect(UserAgents['react-native'].wrapperNames).toContain('react-native-client');
});

describe('UserAgentHelpers.getSDKNameByWrapperOrUserAgent', () => {
test('finds SDK by wrapper name', () => {
const name = UserAgentHelpers.getSDKNameByWrapperOrUserAgent('react-native-client');
expect(name).toBe('React Native SDK');
});

test('finds SDK by user agent', () => {
const name = UserAgentHelpers.getSDKNameByWrapperOrUserAgent('NodeJSClient');
expect(name).toBe('Node.js Server SDK');
});

test('returns undefined for unknown identifier', () => {
const name = UserAgentHelpers.getSDKNameByWrapperOrUserAgent('UnknownIdentifier');
expect(name).toBeUndefined();
});

test('prioritizes wrapper names over user agents', () => {
// In case there's ever a wrapper name that matches a user agent from another SDK,
// we should ensure wrapper names are checked first
const name = UserAgentHelpers.getSDKNameByWrapperOrUserAgent('react-native-client');
expect(name).toBe('React Native SDK');
});

test('finds edge SDK by user agent', () => {
const name = UserAgentHelpers.getSDKNameByWrapperOrUserAgent('CloudflareEdgeSDK');
expect(name).toBe('Cloudflare SDK');
});
});
40 changes: 40 additions & 0 deletions api/sdkmeta/data/user_agents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"akamai-base": {
"userAgents": [
"AkamaiEdgeSDK"
]
},
"akamai-edgekv": {
"userAgents": [
"AkamaiEdgeSDK"
]
},
"cloudflare": {
"userAgents": [
"CloudflareEdgeSDK"
]
},
"fastly": {
"userAgents": [
"FastlyEdgeSDK"
]
},
"node-server": {
"userAgents": [
"NodeJSClient"
]
},
"react-native": {
"userAgents": [
"ReactNativeClient"
],
"wrapperNames": [
"react-native-client"
]
},
"vercel": {
"userAgents": [
"VercelEdgeSDK"
]
}
}
48 changes: 48 additions & 0 deletions api/sdkmeta/sdkmeta.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
_ "embed"
"encoding/json"
"fmt"
"sort"
"time"
)

Expand Down Expand Up @@ -106,6 +107,52 @@ func (r ReleaseList) Latest() Release {
return r[0]
}

//go:embed data/user_agents.json
var userAgentsJSON []byte

// SDKUserAgentMap contains user agent and wrapper information for an SDK
type SDKUserAgentMap struct {
UserAgents []string `json:"userAgents,omitempty"`
WrapperNames []string `json:"wrapperNames,omitempty"`
}

// UserAgents is a map of SDK IDs to their user agent and wrapper information
var UserAgents map[string]SDKUserAgentMap

// GetSDKNameByWrapperOrUserAgent attempts to find an SDK name by first checking wrapper names,
// then user agents, in alphabetical order by SDK ID. Returns the SDK name and true if found,
// empty string and false if not found.
func GetSDKNameByWrapperOrUserAgent(identifier string) (string, bool) {
// Get sorted SDK IDs to ensure consistent ordering
var sdkIDs []string
for sdkID := range UserAgents {
sdkIDs = append(sdkIDs, sdkID)
}
sort.Strings(sdkIDs)

// First check wrapper names
for _, sdkID := range sdkIDs {
info := UserAgents[sdkID]
for _, wrapper := range info.WrapperNames {
if wrapper == identifier {
return Names[sdkID], true
}
}
}

// Then check user agents
for _, sdkID := range sdkIDs {
info := UserAgents[sdkID]
for _, agent := range info.UserAgents {
if agent == identifier {
return Names[sdkID], true
}
}
}

return "", false
}

func panicOnError(err error) {
if err != nil {
panic("couldn't initialize SDK Metadata module: " + err.Error())
Expand All @@ -119,4 +166,5 @@ func init() {
panicOnError(json.Unmarshal(typesJSON, &Types))
panicOnError(json.Unmarshal(releasesJSON, &Releases))
panicOnError(json.Unmarshal(popularityJSON, &Popularity))
panicOnError(json.Unmarshal(userAgentsJSON, &UserAgents))
}
29 changes: 29 additions & 0 deletions api/sdkmeta/sdkmeta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,32 @@ func TestEOLCalculations(t *testing.T) {
assert.True(t, earliest.IsApproachingEOL(earliestEOL.Add(-1*time.Minute), time.Hour))
})
}

func TestUserAgentsAndWrappers(t *testing.T) {
t.Run("user agents map contains expected data", func(t *testing.T) {
nodeInfo := UserAgents["node-server"]
assert.Contains(t, nodeInfo.UserAgents, "NodeJSClient")

reactNativeInfo := UserAgents["react-native"]
assert.Contains(t, reactNativeInfo.UserAgents, "ReactNativeClient")
assert.Contains(t, reactNativeInfo.WrapperNames, "react-native-client")
})

t.Run("GetSDKNameByWrapperOrUserAgent finds by wrapper", func(t *testing.T) {
name, found := GetSDKNameByWrapperOrUserAgent("react-native-client")
assert.True(t, found)
assert.Equal(t, "React Native SDK", name)
})

t.Run("GetSDKNameByWrapperOrUserAgent finds by user agent", func(t *testing.T) {
name, found := GetSDKNameByWrapperOrUserAgent("NodeJSClient")
assert.True(t, found)
assert.Equal(t, "Node.js Server SDK", name)
})

t.Run("GetSDKNameByWrapperOrUserAgent returns false for unknown identifier", func(t *testing.T) {
name, found := GetSDKNameByWrapperOrUserAgent("UnknownIdentifier")
assert.False(t, found)
assert.Empty(t, name)
})
}
40 changes: 40 additions & 0 deletions products/user_agents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"akamai-base": {
"userAgents": [
"AkamaiEdgeSDK"
]
},
"akamai-edgekv": {
"userAgents": [
"AkamaiEdgeSDK"
]
},
"cloudflare": {
"userAgents": [
"CloudflareEdgeSDK"
]
},
"fastly": {
"userAgents": [
"FastlyEdgeSDK"
]
},
"node-server": {
"userAgents": [
"NodeJSClient"
]
},
"react-native": {
"userAgents": [
"ReactNativeClient"
],
"wrapperNames": [
"react-native-client"
]
},
"vercel": {
"userAgents": [
"VercelEdgeSDK"
]
}
}
10 changes: 10 additions & 0 deletions schemas/sdk_metadata.sql
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ CREATE TABLE sdk_popularity (
PRIMARY KEY(id, popularity)
);

CREATE TABLE sdk_user_agents (
id TEXT NOT NULL,
userAgent TEXT
);

CREATE TABLE sdk_wrappers (
id TEXT NOT NULL,
wrapper TEXT
);

INSERT INTO sdk_popularity (id, popularity) VALUES
('react-client-sdk', 1),
('node-server', 2),
Expand Down
38 changes: 38 additions & 0 deletions schemas/user_agents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://launchdarkly.com/sdk-meta/user_agents.json",
"title": "SDK User Agents",
"description": "List of SDK User Agents and Wrappers",
"type": "object",
"$defs": {
"SDKUserAgentMap": {
"type": "object",
"description": "Map of user agents for an SDK",
"properties": {
"userAgents": {
"$ref": "#/$defs/AgentValue"
},
"wrapperNames": {
"$ref": "#/$defs/AgentValue"
}
},
"minProperties": 1,
"additionalProperties": false
},
"AgentValue": {
"type": "array",
"description": "An array of header values, each matching a pattern.",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^[A-Za-z-]*$"
}
}
},
"patternProperties": {
"^[a-z-]+$": {
"$ref": "#/$defs/SDKUserAgentMap"
}
},
"additionalProperties": false
}
4 changes: 4 additions & 0 deletions scripts/generate-products.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ sqlite3 -json metadata.sqlite3 "SELECT * from sdk_feature_info;" |
sqlite3 -json metadata.sqlite3 "SELECT * from sdk_popularity;" |
jq -S 'reduce .[] as $item ({}; .[$item.id] = $item.popularity)' > products/popularity.json

# Generate user agents and wrappers data
sqlite3 -json metadata.sqlite3 "SELECT id, userAgent as value, 'userAgents' as type FROM sdk_user_agents UNION ALL SELECT id, wrapper as value, 'wrapperNames' as type FROM sdk_wrappers;" |
jq -S 'reduce .[] as $item ({}; .[$item.id] = (.[$item.id] // {}) + { ($item.type): ((.[$item.id][$item.type] // []) + [$item.value]) })' > products/user_agents.json

./scripts/eols.sh metadata.sqlite3 |
jq -n 'reduce inputs[] as $input ({}; .[$input.id] += [$input | del(.id)])' > products/releases.json

Expand Down
Loading
Loading