diff --git a/.size-snapshot.json b/.size-snapshot.json index e386ba8..f80894d 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,26 +1,26 @@ { "dist/streamsql.esm.js": { - "bundled": 7741, - "minified": 3828, - "gzipped": 1575, + "bundled": 8133, + "minified": 3944, + "gzipped": 1606, "treeshaked": { "rollup": { "code": 75, "import_statements": 0 }, "webpack": { - "code": 4442 + "code": 4558 } } }, "dist/streamsql.cjs.js": { - "bundled": 9122, - "minified": 4326, - "gzipped": 1641 + "bundled": 9592, + "minified": 4470, + "gzipped": 1679 }, "dist/streamsql.min.js": { - "bundled": 10015, - "minified": 4153, - "gzipped": 1615 + "bundled": 10483, + "minified": 4297, + "gzipped": 1655 } } diff --git a/integration/fixtures/streamsql.min.js b/integration/fixtures/streamsql.min.js index 6a81454..f99583e 100644 --- a/integration/fixtures/streamsql.min.js +++ b/integration/fixtures/streamsql.min.js @@ -1 +1 @@ -!function(t){"function"==typeof define&&define.amd?define("streamsql",t):t()}((function(){"use strict";var t,e=function(t){return new Error("[StreamSQL] Error: "+t)},i=function(t){return t&&t.length?t[0].toUpperCase()+t.slice(1).toLowerCase():t},n=function(){function t(t,e){this.url=t,this.apiKey=e,this.xhr=void 0,this.xhr=this.buildRequest(this.url,this.apiKey)}var i=t.prototype;return i.send=function(t,i){var n=this;return this.xhr=this.buildRequest(this.url,this.apiKey),new Promise((function(r,s){n.xhr.onreadystatechange=function(){if(this.readyState===this.DONE)if(i&&i(this.status,this.responseText),this.status>=200&&this.status<400)try{r(this.response)}catch(t){s(t)}else if(this.status)try{s(this.response)}catch(t){s(t)}else s(e("something went wrong sending XMLHttpRequest"))};try{n.xhr.send(JSON.stringify(t))}catch(t){s(t)}}))},i.buildRequest=function(t,e){var i=new XMLHttpRequest;return i.open("POST",t,!0),i.setRequestHeader("Content-Type","application/json; charset=UTF-8"),i.setRequestHeader("x-streamsql-key",e),i.withCredentials=!0,i.timeout=3e3,i},t}();!function(t){t.None="None",t.Lax="Lax",t.Strict="Strict"}(t||(t={}));var r=function(){function e(){this.simpleCookies=new Map}var n=e.prototype;return n.setCookie=function(e){if(!e.maxAge&&!e.expires){var i=new Date;i.setFullYear(i.getFullYear()+1),e.expires=i.toUTCString()}e.secure=!0,e.sameSite=t.None,this.simpleCookies.set(e.name,e.value),document.cookie=this.toCookieString(e)},n.getCookie=function(t){if(this.simpleCookies.has(t))return this.simpleCookies.get(t);var e="";try{for(var i=t+"=",n=document.cookie.split(";"),r=0;r=200&&this.status<400)try{r(this.response)}catch(t){s(t)}else if(this.status)try{s(this.response)}catch(t){s(t)}else s(e("something went wrong sending XMLHttpRequest"))};try{n.xhr.send(JSON.stringify(t))}catch(t){s(t)}}))},i.buildRequest=function(t,e){var i=new XMLHttpRequest;return i.open("POST",t,!0),i.setRequestHeader("Content-Type","application/json; charset=UTF-8"),i.setRequestHeader("x-streamsql-key",e),i.withCredentials=!0,i.timeout=3e3,i},t}();!function(t){t.None="None",t.Lax="Lax",t.Strict="Strict"}(t||(t={}));var r=function(){function e(){this.simpleCookies=new Map}var n=e.prototype;return n.setCookie=function(e){if(!e.maxAge&&!e.expires){var i=new Date;i.setFullYear(i.getFullYear()+1),e.expires=i.toUTCString()}e.secure=!0,e.sameSite=t.None,this.simpleCookies.set(e.name,e.value),document.cookie=this.toCookieString(e)},n.getCookie=function(t){if(this.simpleCookies.has(t))return this.simpleCookies.get(t);var e="";try{for(var i=t+"=",n=document.cookie.split(";"),r=0;r + + + + + + Test Template Functions + + +

Alt Page

+ + + +
+ +
+ + + + + diff --git a/integration/index.umd.test.js b/integration/index.umd.test.js index 57bd83f..103f666 100644 --- a/integration/index.umd.test.js +++ b/integration/index.umd.test.js @@ -1,5 +1,6 @@ const home = SERVER_URL + '/index.html' const alt = SERVER_URL + '/alt.html' +const templatePage = SERVER_URL + '/template-fns.html' describe('integration', () => { let page @@ -29,8 +30,8 @@ describe('integration', () => { if (postData) { postData = JSON.parse(postData) } - expect(postData.streamName).toMatch('clickstream') - expect(postData.eventTimestamp).toBeGreaterThan(15e9) + expect(postData.stream).toMatch('clickstream') + expect(postData.data.timestamp).toBeGreaterThan(15e9) }, 10000) it('sends page context with url, title, and referrer', async () => { @@ -41,7 +42,7 @@ describe('integration', () => { await page.goto(home) await page.click('button#count-button') postData = postData && JSON.parse(postData) - expect(postData.context).toMatchObject({ + expect(postData.data.context).toMatchObject({ url: await page.url(), title: await page.title(), referrer: '', @@ -50,7 +51,7 @@ describe('integration', () => { await Promise.all([page.waitForNavigation(), page.click('a')]) await page.click('button#send-button') postData = postData && JSON.parse(postData) - expect(postData.context).toMatchObject({ + expect(postData.data.context).toMatchObject({ url: await page.url(), title: await page.title(), referrer: expect.stringMatching(SERVER_URL), @@ -107,27 +108,27 @@ describe('integration', () => { await page.type('input#user-input', userId) await page.click('button#login-button') postData = postData && JSON.parse(postData) - expect(postData.user).toMatchObject({ id: userId }) + expect(postData.data.user).toMatchObject({ id: userId }) postData = undefined // reset for next page // visit the other page, should persist userId await Promise.all([page.waitForNavigation(), page.click('a')]) await page.click('button#send-button') postData = postData && JSON.parse(postData) - expect(postData.user).toMatchObject({ id: userId }) + expect(postData.data.user).toMatchObject({ id: userId }) postData = undefined // do logout, ensure logout sent event does NOT have user id await page.click('#logout-button') postData = postData && JSON.parse(postData) - expect(postData.user).toMatchObject({ id: '' }) + expect(postData.data.user).toMatchObject({ id: '' }) postData = undefined // go back and send event from home page, should NOT have user id await page.goBack() await page.click('#count-button') postData = postData && JSON.parse(postData) - expect(postData.user).toMatchObject({ id: '' }) + expect(postData.data.user).toMatchObject({ id: '' }) }, 20000) it('persists user id when coming back to site', async () => { @@ -144,7 +145,7 @@ describe('integration', () => { await page.type('input#user-input', userId) await page.click('button#login-button') postData = postData && JSON.parse(postData) - expect(postData.user).toMatchObject({ id: userId }) + expect(postData.data.user).toMatchObject({ id: userId }) postData = undefined // reset for next page // visit the other page, should persist userId @@ -153,6 +154,21 @@ describe('integration', () => { await page.goto(alt) await page.click('button#send-button') postData = postData && JSON.parse(postData) - expect(postData.user).toMatchObject({ id: userId }) + expect(postData.data.user).toMatchObject({ id: userId }) }, 8000) + + it('calls template functions onIdentify and onUnidentify', async () => { + const userId = 'user-id' + const responseDivSelector = 'div#identify-response' + // simulate a login that should store id to send w request + await page.goto(templatePage) + await page.type('#input-user-id', userId) + await page.click('button#login-button') + const identifyResponse = await page.$(responseDivSelector) + expect(await identifyResponse.evaluate(node => node.innerText)).toBe(userId); + + await page.click('button#logout-button') + const unidentifyResponse = await page.$(responseDivSelector) + expect(await unidentifyResponse.evaluate(node => node.innerText)).toBe(''); + }) }) diff --git a/src/StreamSQL.ts b/src/StreamSQL.ts index 3325e00..118bd3f 100644 --- a/src/StreamSQL.ts +++ b/src/StreamSQL.ts @@ -36,12 +36,14 @@ export default class StreamSQLClient implements CoreAPI { public identify(userId: string): StreamSQLClient { if (!this.identifier) this.throwNoInitError() this.identifier.setUser(userId) + this.onIdentify(userId) // call template listener return this } public unidentify(): StreamSQLClient { if (!this.identifier) this.throwNoInitError() this.identifier.deleteUser() + this.onUnidentify() // call template listener return this } @@ -57,9 +59,8 @@ export default class StreamSQLClient implements CoreAPI { if (!this.pageCtx || !this.identifier) { this.throwNoInitError() } - return { - streamName: streamName.toLowerCase(), - eventTimestamp: new Date().getTime(), + const streamsqlData = { + timestamp: new Date().getTime(), context: { url: this.pageCtx.location(), referrer: this.pageCtx.referrer(), @@ -69,11 +70,20 @@ export default class StreamSQLClient implements CoreAPI { id: this.identifier.getUser(), // FUTURE: ability to add other user props }, - data: data || {} + } + const _data = data ? Object.assign({}, streamsqlData, data) : streamsqlData + return { + stream: streamName.toLowerCase(), + data: _data } } private throwNoInitError(): never { throw streamsqlErr(`api key must be set first: streamsql.init(apiKey)`) } + + // Template functions. Originally built to allow pixel to listen for events. + // @ts-ignore + public onIdentify(userId: string) {} + public onUnidentify() {} } diff --git a/src/test/streamsql.test.ts b/src/test/streamsql.test.ts index da429f6..aed9d15 100644 --- a/src/test/streamsql.test.ts +++ b/src/test/streamsql.test.ts @@ -132,4 +132,20 @@ describe('StreamSQL Core', () => { }) expect(() => streamsql.sendEvent('mystream')).not.toThrow() }) + + it('calls template listeners for identify, unidentify', () => { + streamsql.init(apiKey) + const onIdentify = jest.fn((userId: string) => userId) + const onUnidentify = jest.fn() + + streamsql.onIdentify = onIdentify + streamsql.onUnidentify = onUnidentify + + streamsql.identify('user-id') + expect(onIdentify).toHaveBeenCalledTimes(1) + expect(onIdentify).toHaveBeenCalledWith('user-id') + + streamsql.unidentify() + expect(onUnidentify).toHaveBeenCalledTimes(1) + }) }) \ No newline at end of file