Skip to content

Commit

Permalink
Merge pull request #1 from fingerprintjs/feature/agent-js-proxy
Browse files Browse the repository at this point in the history
JS agent proxy
  • Loading branch information
TheUnderScorer authored Mar 17, 2023
2 parents 66083a3 + cf3bc90 commit 30aa52a
Show file tree
Hide file tree
Showing 24 changed files with 580 additions and 114 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/coverage-diff.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Check coverage for PR

on:
pull_request:

jobs:
run-tests-check-coverage:
runs-on: ubuntu-20.04
name: Run tests & check coverage
steps:
- uses: actions/checkout@v3
- name: Jest coverage comment
id: coverage
uses: ArtiomTr/jest-coverage-report-action@9f733792c44d05327cb371766bf78a5261e43936
with:
package-manager: yarn
output: report-markdown
- name: Read coverage text report
uses: fingerprintjs/action-coverage-report-md@v1
id: coverage-md
with:
srcBasePath: './'
- uses: marocchino/sticky-pull-request-comment@adca94abcaf73c10466a71cc83ae561fd66d1a56
with:
message: |
${{ steps.coverage.outputs.report }}
<details>
<summary>Show full coverage report</summary>
${{ steps.coverage-md.outputs.markdownReport }}
</details>
46 changes: 46 additions & 0 deletions .github/workflows/coverage-report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Coverage

on:
push:
branches:
- main

jobs:
build-and-run-tests:
runs-on: ubuntu-20.04
name: Build & run tests & publish coverage
steps:
- uses: actions/checkout@v3

- name: Install node
uses: actions/setup-node@v3
with:
node-version-file: '.node-version'
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Dependencies and prepare packages
run: yarn install
env:
CI: true
- name: Run test
run: yarn test

- name: Create Coverage Badges
uses: jaywcjlove/coverage-badges-cli@e07f25709cd25486855c1ba1b26da53576ff3620
with:
source: coverage/coverage-summary.json
output: coverage/lcov-report/badges.svg

- name: Deploy
uses: JamesIves/github-pages-deploy-action@8817a56e5bfec6e2b08345c81f4d422db53a2cdc
with:
branch: gh-pages
folder: ./coverage/lcov-report/
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ typings/
.yarn-integrity

# dotenv environment variables file
.env.test
.env

# parcel-bundler cache (https://parceljs.org/)
.cache
Expand Down
12 changes: 10 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testRegex: '/functions/.+test.tsx?$',
testRegex: '/proxy/.+test.tsx?$',
passWithNoTests: true,
collectCoverageFrom: ['./functions/**/**.ts', '!**/index.ts'],
collectCoverageFrom: ['./proxy/**/**.ts', '!**/index.ts', '!**/config.ts', './management/**/**.ts'],
coverageReporters: ['lcov', 'json-summary', ['text', { file: 'coverage.txt', path: './' }]],
transform: {
'^.+\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.test.json',
},
],
},
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"lint": "eslint --ext .js,.ts --ignore-path .gitignore --max-warnings 0 .",
"lint:fix": "yarn lint --fix",
"test": "jest --coverage",
"test:dts": "tsc --noEmit --isolatedModules dist/*.d.ts",
"test:dts": "tsc --noEmit --isolatedModules dist/**/*.d.ts",
"emulate-storage": "azurite -l ./storage --silent",
"start": "func start"
},
Expand All @@ -31,6 +31,7 @@
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-replace": "^5.0.1",
"@types/jest": "^29.4.1",
"@types/node": "^16.x",
"@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/parser": "^5.44.0",
Expand Down
5 changes: 0 additions & 5 deletions proxy/config.ts

This file was deleted.

46 changes: 20 additions & 26 deletions proxy/agent.ts → proxy/handlers/agent.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { HttpRequest } from '@azure/functions'
import { config } from './config'
import { HttpRequest, Logger } from '@azure/functions'
import { config } from '../utils/config'
import * as https from 'https'
import { updateResponseHeaders } from './headers'
import { filterRequestHeaders, updateResponseHeaders } from '../utils/headers'
import { HttpResponseSimple } from '@azure/functions/types/http'

function getEndpoint(apiKey: string | undefined, version: string, loaderVersion: string | undefined): string {
const lv: string = loaderVersion !== undefined && loaderVersion !== '' ? `/loader_v${loaderVersion}.js` : ''
return `/v${version}/${apiKey}${lv}`
export interface DownloadAgentParams {
httpRequest: HttpRequest
logger: Logger
}

export async function downloadAgent(httpRequest: HttpRequest) {
export async function downloadAgent({ httpRequest, logger }: DownloadAgentParams) {
const apiKey = httpRequest.query.apiKey
const version = httpRequest.query.version
const loaderVersion = httpRequest.query.loaderVersion
Expand All @@ -19,26 +19,17 @@ export async function downloadAgent(httpRequest: HttpRequest) {
const url = new URL(`https://${config.fpdcdn}`)
url.pathname = getEndpoint(apiKey, version, loaderVersion)

const headers = {
...httpRequest.headers,
}
logger.verbose('Downloading agent from', url.toString())

// TODO - Extract this into separate function
delete headers['host']
delete headers['content-length']
delete headers['transfer-encoding']
delete headers['via']
const headers = filterRequestHeaders(httpRequest.headers)

return new Promise<HttpResponseSimple & { isRaw?: boolean }>((resolve) => {
return new Promise<HttpResponseSimple>((resolve) => {
const data: any[] = []

console.debug('Downloading agent from', url.toString())

const request = https.request(
url,
{
method: 'GET',
// TODO Filter headers
headers,
},
(response) => {
Expand All @@ -53,31 +44,34 @@ export async function downloadAgent(httpRequest: HttpRequest) {

response.on('end', () => {
const body = Buffer.concat(data)
const responseHeaders = updateResponseHeaders(response.headers, domain)

resolve({
status: response.statusCode ? response.statusCode.toString() : '500',
// TODO Filter headers
headers: updateResponseHeaders(response.headers, domain),
headers: responseHeaders,
body: new Uint8Array(body),
isRaw: true,
})
})
},
)

request.on('error', (error) => {
console.error('unable to download agent', { error })
logger.error('unable to download agent', { error })

resolve({
status: '500',
headers: {
'Content-Type': 'application/json',
'Content-Type': 'text/plain',
},
// TODO Generate error response with our integrations format
body: error,
body: 'error',
})
})

request.end()
})
}

function getEndpoint(apiKey: string | undefined, version: string, loaderVersion: string | undefined): string {
const lv: string = loaderVersion !== undefined && loaderVersion !== '' ? `/loader_v${loaderVersion}.js` : ''
return `/v${version}/${apiKey}${lv}`
}
4 changes: 2 additions & 2 deletions proxy/ingress.ts → proxy/handlers/ingress.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HttpRequest } from '@azure/functions'
import { config } from './config'
import { config } from '../utils/config'
import * as https from 'https'
import { updateResponseHeaders } from './headers'
import { updateResponseHeaders } from '../utils/headers'
import { HttpResponseSimple } from '@azure/functions/types/http'

export function handleIngress(httpRequest: HttpRequest) {
Expand Down
58 changes: 0 additions & 58 deletions proxy/headers.ts

This file was deleted.

12 changes: 9 additions & 3 deletions proxy/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { AzureFunction, Context, HttpRequest } from '@azure/functions'
import { downloadAgent } from './agent'
import { handleIngress } from './ingress'
import { downloadAgent } from './handlers/agent'
import { handleIngress } from './handlers/ingress'

const httpTrigger: AzureFunction = async (context: Context, req: HttpRequest): Promise<void> => {
context.log.verbose('Handling request', {
req,
context,
})

const path = req.params?.restOfPath

// TODO Resolve paths using customer variables
switch (path) {
case 'client': {
context.res = await downloadAgent(req)
context.res = await downloadAgent({ httpRequest: req, logger: context.log })

break
}
Expand Down
15 changes: 15 additions & 0 deletions proxy/utils/cacheControl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { updateCacheControlHeader } from './cacheControl'

describe('updateCacheControlHeader', () => {
test('adjust max-age to lower value', () => {
expect(updateCacheControlHeader('public, max-age=36000, s-maxage=36000')).toBe('public, max-age=3600, s-maxage=60')
})

test('keep existing smaller value', () => {
expect(updateCacheControlHeader('public, max-age=600, s-maxage=600')).toBe('public, max-age=600, s-maxage=60')
})

test('add max age if not exist', () => {
expect(updateCacheControlHeader('no-cache')).toBe('no-cache, max-age=3600, s-maxage=60')
})
})
26 changes: 26 additions & 0 deletions proxy/utils/cacheControl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const CACHE_MAX_AGE = 3600
const SHARED_CACHE_MAX_AGE = 60

export function updateCacheControlHeader(headerValue: string): string {
let result = updateCacheControlAge(headerValue, 'max-age', CACHE_MAX_AGE)
result = updateCacheControlAge(result, 's-maxage', SHARED_CACHE_MAX_AGE)

return result
}

function updateCacheControlAge(headerValue: string, type: 'max-age' | 's-maxage', cacheMaxAge: number): string {
const cacheControlDirectives = headerValue.split(', ')
const maxAgeIndex = cacheControlDirectives.findIndex(
(directive) => directive.split('=')[0].trim().toLowerCase() === type,
)

if (maxAgeIndex === -1) {
cacheControlDirectives.push(`${type}=${cacheMaxAge}`)
} else {
const oldMaxAge = Number(cacheControlDirectives[maxAgeIndex].split('=')[1])
const newMaxAge = Math.min(cacheMaxAge, oldMaxAge)
cacheControlDirectives[maxAgeIndex] = `${type}=${newMaxAge}`
}

return cacheControlDirectives.join(', ')
}
4 changes: 4 additions & 0 deletions proxy/utils/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const config = {
fpdcdn: '__FPCDN__',
ingressApi: '__INGRESS_API__',
}
Loading

0 comments on commit 30aa52a

Please sign in to comment.