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
83 changes: 83 additions & 0 deletions __tests__/api/acks_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* @jest-environment jsdom
*/

import AcksAPI from '../../src/api/acks'
import Hellotext from '../../src/hellotext'
import { Configuration } from '../../src/core'

describe('AcksAPI', () => {
beforeEach(() => {
jest.useFakeTimers().setSystemTime(new Date('2026-06-19T12:00:00.000Z'))

Configuration.apiRoot = 'https://api.hellotext.test/v1'
Hellotext.business = {
id: 'business-id',
}

jest.spyOn(Hellotext, 'session', 'get').mockReturnValue('session-123')

global.fetch = jest.fn()
})

afterEach(() => {
jest.useRealTimers()
jest.restoreAllMocks()
Configuration.apiRoot = 'https://api.hellotext.com/v1'
})

it('posts the supplied UTM params with the session ack payload', async () => {
await AcksAPI.send({
utm_params: {
source: 'google',
medium: 'cpc',
campaign: 'summer-sale',
term: 'sandals',
content: 'hero-button',
},
})

expect(global.fetch).toHaveBeenCalledWith('https://api.hellotext.test/v1/public/acks', {
method: 'POST',
headers: {
Authorization: 'Bearer business-id',
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
utm_params: {
source: 'google',
medium: 'cpc',
campaign: 'summer-sale',
term: 'sandals',
content: 'hero-button',
},
session: 'session-123',
at: '2026-06-19T12:00:00.000Z',
}),
keepalive: true,
})
})

it('keeps the current SDK session and timestamp authoritative over supplied params', async () => {
await AcksAPI.send({
session: 'stale-session',
at: '2020-01-01T00:00:00.000Z',
utm_params: {
source: 'facebook',
medium: 'social',
},
})

const requestBody = JSON.parse(global.fetch.mock.calls[0][1].body)

expect(requestBody).toEqual({
session: 'session-123',
at: '2026-06-19T12:00:00.000Z',
utm_params: {
source: 'facebook',
medium: 'social',
},
})
})
})
83 changes: 83 additions & 0 deletions __tests__/models/session_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,74 @@ describe('Session', () => {
expect(API.acks.send).toHaveBeenCalledTimes(1)
})

it('sends the initialized page UTM params with the first ack', () => {
const page = {
get utmParams() {
return {
source: 'google',
medium: 'cpc',
campaign: 'summer-sale',
term: 'sandals',
content: 'hero-button',
}
},
}

Configuration.session = 'new-session'
Session.initialize(page)

expect(API.acks.send).toHaveBeenCalledTimes(1)
expect(API.acks.send).toHaveBeenCalledWith({
utm_params: {
source: 'google',
medium: 'cpc',
campaign: 'summer-sale',
term: 'sandals',
content: 'hero-button',
},
})
})

it('sends an empty UTM payload when the initialized page has no UTM params', () => {
Configuration.session = 'new-session'
Session.initialize({})

expect(API.acks.send).toHaveBeenCalledTimes(1)
expect(API.acks.send).toHaveBeenCalledWith({
utm_params: {},
})
})

it('uses the latest initialized page context when a changed session is re-acked', () => {
Configuration.session = 'first-session'
Session.initialize({
utmParams: {
source: 'google',
medium: 'cpc',
},
})

API.acks.send.mockClear()

Configuration.session = 'second-session'
Session.initialize({
utmParams: {
source: 'facebook',
medium: 'social',
campaign: 'retargeting',
},
})

expect(API.acks.send).toHaveBeenCalledTimes(1)
expect(API.acks.send).toHaveBeenCalledWith({
utm_params: {
source: 'facebook',
medium: 'social',
campaign: 'retargeting',
},
})
})

it('sets hello_session_ack_at cookie after sending an ack', () => {
Session.session = 'new-session'

Expand All @@ -417,6 +485,21 @@ describe('Session', () => {
expect(API.acks.send).not.toHaveBeenCalled()
})

it('does not send initialized page UTM params when the existing session is already acked', () => {
Cookies.set('hello_session', 'existing-session')
Cookies.set('hello_session_ack_at', new Date().toISOString())

Configuration.session = 'existing-session'
Session.initialize({
utmParams: {
source: 'google',
medium: 'cpc',
},
})

expect(API.acks.send).not.toHaveBeenCalled()
})

it('does not delete hello_session_ack_at when the same session is set again', () => {
const ackedAt = new Date().toISOString()
Cookies.set('hello_session', 'existing-session')
Expand Down
2 changes: 1 addition & 1 deletion dist/hellotext.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion lib/api/acks.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ let AcksAPI = /*#__PURE__*/function () {
}
}, {
key: "send",
value: async function send() {
value: async function send(params = {}) {
const payload = {
...params,
session: _hellotext.default.session,
at: new Date().toISOString()
};
Expand Down
8 changes: 6 additions & 2 deletions lib/api/acks.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
Expand All @@ -20,10 +23,11 @@ var AcksAPI = /*#__PURE__*/function () {
key: "send",
value: function () {
var _send = _asyncToGenerator(function* () {
var payload = {
var params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var payload = _objectSpread(_objectSpread({}, params), {}, {
session: Hellotext.session,
at: new Date().toISOString()
};
});
fetch(this.endpoint, {
method: 'POST',
headers: Hellotext.headers,
Expand Down
4 changes: 2 additions & 2 deletions lib/hellotext.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ let Hellotext = /*#__PURE__*/function () {
*/
async function initialize(business, config = {}) {
this.business = new _models.Business(business);
_core.Configuration.assign(config);
_models.Session.initialize();
this.page = new _models.Page();
_core.Configuration.assign(config);
_models.Session.initialize(this.page);
this.forms = new _models.FormCollection();
this.query = new _models.Query();
const businessData = await this.business.hydrate();
Expand Down
4 changes: 2 additions & 2 deletions lib/hellotext.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ var Hellotext = /*#__PURE__*/function () {
var _initialize = _asyncToGenerator(function* (business) {
var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
this.business = new Business(business);
Configuration.assign(config);
Session.initialize();
this.page = new Page();
Configuration.assign(config);
Session.initialize(this.page);
this.forms = new FormCollection();
this.query = new Query();
var businessData = yield this.business.hydrate();
Expand Down
19 changes: 17 additions & 2 deletions lib/models/session.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
exports.Session = void 0;
var _core = require("../core");
var _cookies = require("./cookies");
var _page2 = require("./page");
var _query2 = require("./query");
var _api = _interopRequireDefault(require("../api"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
Expand All @@ -19,6 +20,7 @@ var id = 0;
function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; }
var _session = /*#__PURE__*/_classPrivateFieldLooseKey("session");
var _query = /*#__PURE__*/_classPrivateFieldLooseKey("query");
var _page = /*#__PURE__*/_classPrivateFieldLooseKey("page");
let Session = /*#__PURE__*/function () {
function Session() {
_classCallCheck(this, Session);
Expand All @@ -36,14 +38,23 @@ let Session = /*#__PURE__*/function () {
_cookies.Cookies.delete('hello_session_ack_at');
}
if (!_cookies.Cookies.get('hello_session_ack_at')) {
_api.default.acks.send();
_api.default.acks.send(this.ackPayload);
_cookies.Cookies.set('hello_session_ack_at', new Date().toISOString());
}
return _classPrivateFieldLooseBase(this, _session)[_session];
}
}, {
key: "ackPayload",
get: function () {
var _classPrivateFieldLoo;
return {
utm_params: ((_classPrivateFieldLoo = _classPrivateFieldLooseBase(this, _page)[_page]) === null || _classPrivateFieldLoo === void 0 ? void 0 : _classPrivateFieldLoo.utmParams) || {}
};
}
}, {
key: "initialize",
value: function initialize() {
value: function initialize(page = new _page2.Page()) {
_classPrivateFieldLooseBase(this, _page)[_page] = page;
_classPrivateFieldLooseBase(this, _query)[_query] = new _query2.Query();
this.session = _classPrivateFieldLooseBase(this, _query)[_query].session || _core.Configuration.session || _cookies.Cookies.get('hello_session');
if (!this.session && _core.Configuration.autoGenerateSession) {
Expand All @@ -61,4 +72,8 @@ Object.defineProperty(Session, _session, {
Object.defineProperty(Session, _query, {
writable: true,
value: void 0
});
Object.defineProperty(Session, _page, {
writable: true,
value: void 0
});
18 changes: 17 additions & 1 deletion lib/models/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ var id = 0;
function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; }
import { Configuration } from '../core';
import { Cookies } from './cookies';
import { Page } from './page';
import { Query } from './query';
import API from '../api';
var _session = /*#__PURE__*/_classPrivateFieldLooseKey("session");
var _query = /*#__PURE__*/_classPrivateFieldLooseKey("query");
var _page = /*#__PURE__*/_classPrivateFieldLooseKey("page");
var Session = /*#__PURE__*/function () {
function Session() {
_classCallCheck(this, Session);
Expand All @@ -29,14 +31,24 @@ var Session = /*#__PURE__*/function () {
Cookies.delete('hello_session_ack_at');
}
if (!Cookies.get('hello_session_ack_at')) {
API.acks.send();
API.acks.send(this.ackPayload);
Cookies.set('hello_session_ack_at', new Date().toISOString());
}
return _classPrivateFieldLooseBase(this, _session)[_session];
}
}, {
key: "ackPayload",
get: function get() {
var _classPrivateFieldLoo;
return {
utm_params: ((_classPrivateFieldLoo = _classPrivateFieldLooseBase(this, _page)[_page]) === null || _classPrivateFieldLoo === void 0 ? void 0 : _classPrivateFieldLoo.utmParams) || {}
};
}
}, {
key: "initialize",
value: function initialize() {
var page = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Page();
_classPrivateFieldLooseBase(this, _page)[_page] = page;
_classPrivateFieldLooseBase(this, _query)[_query] = new Query();
this.session = _classPrivateFieldLooseBase(this, _query)[_query].session || Configuration.session || Cookies.get('hello_session');
if (!this.session && Configuration.autoGenerateSession) {
Expand All @@ -54,4 +66,8 @@ Object.defineProperty(Session, _query, {
writable: true,
value: void 0
});
Object.defineProperty(Session, _page, {
writable: true,
value: void 0
});
export { Session };
3 changes: 2 additions & 1 deletion src/api/acks.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ class AcksAPI {
return Configuration.endpoint('public/acks')
}

static async send() {
static async send(params = {}) {
const payload = {
...params,
session: Hellotext.session,
at: new Date().toISOString(),
}
Expand Down
Loading