diff --git a/CHANGELOG.md b/CHANGELOG.md index d9069cf8..4d36b559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to the LaunchDarkly client-side JavaScript SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.1.0] - 2018-05-31 +### Added: +- The client now sends the current SDK version to LaunchDarkly in an HTTP header. This information will be visible in a future version of the LaunchDarkly UI. + +### Fixed: +- Fixed a bug that caused summary events to combine the counts for flag evaluations that produced the flag's first variation (variation index 0) with the counts for flag evaluations that fell through to the default value. + ## [2.0.0] - 2018-05-25 ### Changed - To reduce the network bandwidth used for analytics events, feature request events are now sent as counters rather than individual events, and user details are now sent only at intervals rather than in each event. These behaviors can be modified through the LaunchDarkly UI and with the new configuration option `inlineUsersInEvents`. For more details, see [Analytics Data Stream Reference](https://docs.launchdarkly.com/v2.0/docs/analytics-data-stream-reference). diff --git a/package.json b/package.json index be47a700..62a696fe 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ldclient-js", - "version": "2.0.0", + "version": "2.1.0", "description": "LaunchDarkly SDK for JavaScript", "author": "LaunchDarkly ", "license": "Apache-2.0", diff --git a/src/EventSender.js b/src/EventSender.js index bae56320..6b155f28 100644 --- a/src/EventSender.js +++ b/src/EventSender.js @@ -34,7 +34,9 @@ export default function EventSender(eventsUrl, environmentId, forceHasCors, imag if (usePost) { const xhr = new XMLHttpRequest(); xhr.open('POST', postUrl, !sync); + utils.addLDHeaders(xhr); xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('X-LaunchDarkly-Event-Schema', '3'); if (!sync) { xhr.addEventListener('load', () => { diff --git a/src/EventSummarizer.js b/src/EventSummarizer.js index 2e82259c..2a5b833e 100644 --- a/src/EventSummarizer.js +++ b/src/EventSummarizer.js @@ -7,7 +7,12 @@ export default function EventSummarizer() { es.summarizeEvent = function(event) { if (event.kind === 'feature') { - const counterKey = event.key + ':' + (event.variation || '') + (event.version || ''); + const counterKey = + event.key + + ':' + + (event.variation !== null && event.variation !== undefined ? event.variation : '') + + ':' + + (event.version !== null && event.version !== undefined ? event.version : ''); const counterVal = counters[counterKey]; if (counterVal) { counterVal.count = counterVal.count + 1; diff --git a/src/Requestor.js b/src/Requestor.js index a6666c52..2ad50c19 100644 --- a/src/Requestor.js +++ b/src/Requestor.js @@ -29,9 +29,11 @@ function fetchJSON(endpoint, body, callback) { if (body) { xhr.open('REPORT', endpoint); xhr.setRequestHeader('Content-Type', 'application/json'); + utils.addLDHeaders(xhr); xhr.send(JSON.stringify(body)); } else { xhr.open('GET', endpoint); + utils.addLDHeaders(xhr); xhr.send(); } diff --git a/src/__tests__/EventSender-test.js b/src/__tests__/EventSender-test.js index 28ada581..5218a401 100644 --- a/src/__tests__/EventSender-test.js +++ b/src/__tests__/EventSender-test.js @@ -2,6 +2,7 @@ import Base64 from 'Base64'; import sinon from 'sinon'; import EventSender from '../EventSender'; +import * as utils from '../utils'; describe('EventSender', () => { let sandbox; @@ -127,5 +128,13 @@ describe('EventSender', () => { expect(r.method).toEqual('POST'); expect(JSON.parse(r.requestBody)).toEqual(events); }); + + it('should send custom user-agent header', () => { + const sender = EventSender(eventsUrl, envId, true); + const event = { kind: 'identify', key: 'userKey' }; + sender.sendEvents([event], true); + lastRequest().respond(); + expect(lastRequest().requestHeaders['X-LaunchDarkly-User-Agent']).toEqual(utils.getLDUserAgentString()); + }); }); }); diff --git a/src/__tests__/EventSummarizer-test.js b/src/__tests__/EventSummarizer-test.js index 4d506b18..98d323f3 100644 --- a/src/__tests__/EventSummarizer-test.js +++ b/src/__tests__/EventSummarizer-test.js @@ -31,20 +31,21 @@ describe('EventSummarizer', () => { expect(data.endDate).toEqual(2000); }); + function makeEvent(key, version, variation, value, defaultVal) { + return { + kind: 'feature', + creationDate: 1000, + key: key, + version: version, + user: user, + variation: variation, + value: value, + default: defaultVal, + }; + } + it('increments counters for feature events', () => { const es = EventSummarizer(); - function makeEvent(key, version, variation, value, defaultVal) { - return { - kind: 'feature', - creationDate: 1000, - key: key, - version: version, - user: user, - variation: variation, - value: value, - default: defaultVal, - }; - } const event1 = makeEvent('key1', 11, 1, 100, 111); const event2 = makeEvent('key1', 11, 2, 200, 111); const event3 = makeEvent('key2', 22, 1, 999, 222); @@ -77,4 +78,24 @@ describe('EventSummarizer', () => { }; expect(data.features).toEqual(expectedFeatures); }); + + it('distinguishes between zero and null/undefined in feature variation', () => { + const es = EventSummarizer(); + const event1 = makeEvent('key1', 11, 0, 100, 111); + const event2 = makeEvent('key1', 11, null, 111, 111); + const event3 = makeEvent('key1', 11, undefined, 111, 111); + es.summarizeEvent(event1); + es.summarizeEvent(event2); + es.summarizeEvent(event3); + const data = es.getSummary(); + + data.features.key1.counters.sort((a, b) => a.value - b.value); + const expectedFeatures = { + key1: { + default: 111, + counters: [{ variation: 0, value: 100, version: 11, count: 1 }, { value: 111, version: 11, count: 2 }], + }, + }; + expect(data.features).toEqual(expectedFeatures); + }); }); diff --git a/src/__tests__/Requestor-test.js b/src/__tests__/Requestor-test.js index 406eb732..c2c0e30d 100644 --- a/src/__tests__/Requestor-test.js +++ b/src/__tests__/Requestor-test.js @@ -1,5 +1,6 @@ import sinon from 'sinon'; import Requestor from '../Requestor'; +import * as utils from '../utils'; describe('Requestor', () => { let server; @@ -83,4 +84,22 @@ describe('Requestor', () => { expect(handleFour.calledOnce).toEqual(true); expect(handleFive.calledOnce).toEqual(true); }); + + it('should send custom user-agent header in GET mode', () => { + const requestor = Requestor('http://requestee', 'FAKE_ENV', false); + const user = { key: 'foo' }; + requestor.fetchFlagSettings(user, 'hash1', sinon.spy()); + + expect(server.requests.length).toEqual(1); + expect(server.requests[0].requestHeaders['X-LaunchDarkly-User-Agent']).toEqual(utils.getLDUserAgentString()); + }); + + it('should send custom user-agent header in REPORT mode', () => { + const requestor = Requestor('http://requestee', 'FAKE_ENV', true); + const user = { key: 'foo' }; + requestor.fetchFlagSettings(user, 'hash1', sinon.spy()); + + expect(server.requests.length).toEqual(1); + expect(server.requests[0].requestHeaders['X-LaunchDarkly-User-Agent']).toEqual(utils.getLDUserAgentString()); + }); }); diff --git a/src/utils.js b/src/utils.js index 1ab799ae..a84a22d9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -115,3 +115,11 @@ export function chunkUserEventsForUrl(maxLength, events) { return allChunks; } + +export function getLDUserAgentString() { + return 'JSClient/' + VERSION; +} + +export function addLDHeaders(xhr) { + xhr.setRequestHeader('X-LaunchDarkly-User-Agent', getLDUserAgentString()); +}