From fb9e3350523abad281703e0dfd2bd30efd76b4e2 Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Thu, 16 Nov 2023 17:25:27 +0100 Subject: [PATCH 01/14] feat: allow custom SSE host (#79) --- README.md | 3 +- src/config.ts | 3 +- src/main.ts | 6 +- src/sse.ts | 26 +++++--- src/types.ts | 1 + test/sse.test.ts | 154 ++++++++++++++++++++++++++++++++++++++++------- 6 files changed, 158 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index f6dc7451..229a404e 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,8 @@ Supply these to the `init` call (2nd argument) { debug?: false, // enable debug mode to log all info and errors persistUser?: true | false // default value depends on environment, see below under "persisting users" - host?: "https://tracking.bucket.co", // don't change this + host?: "https://tracking.bucket.co", + sseHost?: "https://livemessaging.bucket.co" } ``` diff --git a/src/config.ts b/src/config.ts index 7bed0c8d..95d298ac 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,3 +1,2 @@ export const TRACKING_HOST = "https://tracking.bucket.co"; -export const ABLY_REST_HOST = "https://livemessaging.bucket.co"; -export const ABLY_REALTIME_HOST = ABLY_REST_HOST; +export const SSE_REALTIME_HOST = "https://livemessaging.bucket.co"; diff --git a/src/main.ts b/src/main.ts index e72c8265..37d347de 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import { isForNode } from "is-bundling-for-browser-or-node"; import { version } from "../package.json"; import type { FeedbackPosition, FeedbackTranslations } from "./feedback/types"; -import { TRACKING_HOST } from "./config"; +import { SSE_REALTIME_HOST, TRACKING_HOST } from "./config"; import { defaultFeedbackPromptHandler } from "./default-feedback-prompt-handler"; import * as feedbackLib from "./feedback"; import { @@ -45,6 +45,7 @@ export default function main() { let debug = false; let trackingKey: string | undefined = undefined; let trackingHost: string = TRACKING_HOST; + let sseHost: string = SSE_REALTIME_HOST; let sessionUserId: string | undefined = undefined; let persistUser: boolean = !isForNode; let liveSatisfactionActive: boolean = false; @@ -112,6 +113,7 @@ export default function main() { if (options.debug) debug = options.debug; if (options.host) trackingHost = options.host; + if (options.sseHost) sseHost = options.sseHost; if (options.feedback?.ui?.position) { feedbackPosition = options.feedback?.ui?.position; @@ -295,7 +297,7 @@ export default function main() { userId, body.channel, (message) => handleFeedbackPromptRequest(userId!, message), - { debug }, + { debug, sseHost }, ); feedbackPromptingUserId = userId; diff --git a/src/sse.ts b/src/sse.ts index b1258e57..a6dae33e 100644 --- a/src/sse.ts +++ b/src/sse.ts @@ -1,6 +1,6 @@ import fetch from "cross-fetch"; -import { ABLY_REALTIME_HOST, ABLY_REST_HOST } from "./config"; +import { SSE_REALTIME_HOST } from "./config"; interface AblyTokenDetails { token: string; @@ -23,6 +23,7 @@ export class AblySSEChannel { private userId: string, private channel: string, private ablyAuthUrl: string, + private sseHost: string, private messageHandler: (message: any) => void, options?: { debug?: boolean; @@ -75,9 +76,8 @@ export class AblySSEChannel { private async refreshToken() { const tokenRequest = await this.refreshTokenRequest(); - const res = await fetch( - `${ABLY_REST_HOST}/keys/${encodeURIComponent( + `${this.sseHost}/keys/${encodeURIComponent( tokenRequest.keyName, )}/requestToken`, { @@ -176,7 +176,7 @@ export class AblySSEChannel { const token = await this.refreshToken(); this.eventSource = new EventSource( - `${ABLY_REALTIME_HOST}/sse?v=1.2&accessToken=${encodeURIComponent( + `${this.sseHost}/sse?v=1.2&accessToken=${encodeURIComponent( token.token, )}&channels=${encodeURIComponent(this.channel)}&rewind=1`, ); @@ -267,11 +267,21 @@ export function openAblySSEChannel( userId: string, channel: string, callback: (req: object) => void, - options?: { debug?: boolean; retryInterval?: number; retryCount?: number }, + options?: { + debug?: boolean; + retryInterval?: number; + retryCount?: number; + sseHost?: string; + }, ) { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, callback, { - debug: options?.debug, - }); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + options?.sseHost || SSE_REALTIME_HOST, + callback, + { debug: options?.debug }, + ); sse.open(); diff --git a/src/types.ts b/src/types.ts index a45c44cb..630856a2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,6 +10,7 @@ export type Key = string; export type Options = { persistUser?: boolean; host?: string; + sseHost?: string; debug?: boolean; feedback?: { /** diff --git a/test/sse.test.ts b/test/sse.test.ts index 676bd9ef..b6c5edfc 100644 --- a/test/sse.test.ts +++ b/test/sse.test.ts @@ -2,7 +2,6 @@ import flushPromises from "flush-promises"; import nock from "nock"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { ABLY_REST_HOST } from "../src/config"; import { AblySSEChannel, closeAblySSEChannel, @@ -10,6 +9,7 @@ import { } from "../src/sse"; const ablyAuthUrl = "https://example.com/123/feedback/prompting-auth"; +const sseHost = "https://ssehost.com"; const tokenRequest = { keyName: "key-name", other: "other", @@ -38,7 +38,7 @@ function setupAuthNock(success: boolean | number) { } function setupTokenNock(success: boolean) { - const n = nock(`${ABLY_REST_HOST}/keys/${tokenRequest.keyName}`).post( + const n = nock(`${sseHost}/keys/${tokenRequest.keyName}`).post( /.*\/requestToken/, { ...tokenRequest, @@ -61,7 +61,13 @@ describe("connection handling", () => { }); test("rejects if auth endpoint is not success", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); setupAuthNock(false); @@ -71,7 +77,13 @@ describe("connection handling", () => { }); test("rejects if auth endpoint is not 200", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); setupAuthNock(403); @@ -81,7 +93,13 @@ describe("connection handling", () => { }); test("rejects if token endpoint rejects", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); setupAuthNock(true); setupTokenNock(false); @@ -92,7 +110,13 @@ describe("connection handling", () => { }); test("obtains token, connects and subscribes, then closes", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); const addEventListener = vi.fn(); const close = vi.fn(); @@ -128,7 +152,13 @@ describe("connection handling", () => { }); test("does not try to re-connect if already connecting", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); const close = vi.fn(); vi.mocked(window.EventSource).mockReturnValue({ @@ -150,7 +180,13 @@ describe("connection handling", () => { }); test("does not re-connect if already connected", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); const close = vi.fn(); vi.mocked(window.EventSource).mockReturnValue({ @@ -169,7 +205,13 @@ describe("connection handling", () => { }); test("disconnects only if connected", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); const close = vi.fn(); vi.mocked(window.EventSource).mockReturnValue({ @@ -197,7 +239,13 @@ describe("message handling", () => { test("passes message to callback", async () => { const callback = vi.fn(); - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, callback); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + callback, + ); let messageCallback: ((e: Event) => void) | undefined = undefined; const addEventListener = (event: string, cb: (e: Event) => void) => { @@ -231,7 +279,13 @@ describe("message handling", () => { }); test("disconnects on unknown event source errors without data", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); let errorCallback: ((e: Event) => Promise) | undefined = undefined; const addEventListener = (event: string, cb: (e: Event) => void) => { @@ -255,8 +309,14 @@ describe("message handling", () => { }); test("disconnects on unknown event source errors with data", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); - + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); + sseHost; let errorCallback: ((e: Event) => Promise) | undefined = undefined; const addEventListener = (event: string, cb: (e: Event) => void) => { if (event === "error") { @@ -284,7 +344,13 @@ describe("message handling", () => { }); test("disconnects when ably reports token is expired", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); let errorCallback: ((e: Event) => Promise) | undefined = undefined; const addEventListener = (event: string, cb: (e: Event) => void) => { @@ -330,7 +396,13 @@ describe("automatic retries", () => { }); test("opens and connects to a channel", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); const n1 = setupAuthNock(true); const n2 = setupTokenNock(true); @@ -346,7 +418,13 @@ describe("automatic retries", () => { }); test("opens and connects later to a failed channel", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); const n1 = setupAuthNock(false); @@ -374,7 +452,13 @@ describe("automatic retries", () => { }); test("only, ever, allow one connection to SSE", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); const n1 = setupAuthNock(true); const n2 = setupTokenNock(true); @@ -401,7 +485,13 @@ describe("automatic retries", () => { }); test("resets retry count on successfull connect", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); // mock event source let errorCallback: ((e: Event) => Promise) | undefined = undefined; @@ -457,7 +547,13 @@ describe("automatic retries", () => { }); test("reconnects if manually disconnected", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); vi.mocked(window.EventSource).mockReturnValue({ addEventListener: vi.fn(), @@ -494,7 +590,13 @@ describe("automatic retries", () => { }); test("opens and does not connect later to a failed channel if no retries", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); const n1 = setupAuthNock(false); @@ -516,7 +618,13 @@ describe("automatic retries", () => { }); test("closes an open channel", async () => { - const sse = new AblySSEChannel(userId, channel, ablyAuthUrl, vi.fn()); + const sse = new AblySSEChannel( + userId, + channel, + ablyAuthUrl, + sseHost, + vi.fn(), + ); const n1 = setupAuthNock(true); const n2 = setupTokenNock(true); @@ -563,7 +671,9 @@ describe("helper open and close functions", () => { const n1 = setupAuthNock(true); const n2 = setupTokenNock(true); - const sse = openAblySSEChannel(ablyAuthUrl, userId, channel, vi.fn()); + const sse = openAblySSEChannel(ablyAuthUrl, userId, channel, vi.fn(), { + sseHost, + }); expect(sse.isActive()).toBe(true); From 73a96f707750f41586694b90ab685c61cde7315c Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Fri, 17 Nov 2023 11:11:20 +0100 Subject: [PATCH 02/14] fix: removing unmaintained changelog (#81) --- CHANGELOG.md | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index d3c03afd..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,25 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -## [1.1.1](https://github.com/bucketco/bucket-tracking-sdk/compare/v1.1.0...v1.1.1) - 2023-03-03 - -### Changed - -- Change feedback from 'sentiment' to 'score' (closed beta) - -## [1.1.0](https://github.com/bucketco/bucket-tracking-sdk/compare/v1.0.0...v1.1.0) - 2023-01-12 - -### Added - -- Send feature feedback to Bucket (closed beta) - -### Changed - -- Improved typing for errors - -## 1.0.0 - -### Added - -- Initial release From 3378b2cff3141933c08e72fdfdba01d59b5fcb49 Mon Sep 17 00:00:00 2001 From: Richard Foster Date: Fri, 17 Nov 2023 12:06:33 +0000 Subject: [PATCH 03/14] Fix feedback UI init options not getting applied to prompts (#82) --- src/default-feedback-prompt-handler.ts | 22 +++---- src/main.ts | 4 +- src/types.ts | 11 ++-- test/e2e/feedback-widget.browser.spec.ts | 77 +++++++++++++++++++++++- 4 files changed, 94 insertions(+), 20 deletions(-) diff --git a/src/default-feedback-prompt-handler.ts b/src/default-feedback-prompt-handler.ts index b9a637cf..8bde3152 100644 --- a/src/default-feedback-prompt-handler.ts +++ b/src/default-feedback-prompt-handler.ts @@ -1,13 +1,13 @@ -import { FeedbackPrompt, FeedbackPromptHandler } from "./types"; +import { + FeedbackPrompt, + FeedbackPromptHandler, + FeedbackPromptHandlerOpenFeedbackFormOptions, +} from "./types"; -export const defaultFeedbackPromptHandler: FeedbackPromptHandler = ( - _prompt: FeedbackPrompt, - handlers, -) => { - handlers.openFeedbackForm({ - position: { - type: "DIALOG", - placement: "bottom-right", - }, - }); +export const createDefaultFeedbackPromptHandler = ( + options: FeedbackPromptHandlerOpenFeedbackFormOptions = {}, +): FeedbackPromptHandler => { + return (_prompt: FeedbackPrompt, handlers) => { + handlers.openFeedbackForm(options); + }; }; diff --git a/src/main.ts b/src/main.ts index 37d347de..de009ac3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,7 +5,7 @@ import { version } from "../package.json"; import type { FeedbackPosition, FeedbackTranslations } from "./feedback/types"; import { SSE_REALTIME_HOST, TRACKING_HOST } from "./config"; -import { defaultFeedbackPromptHandler } from "./default-feedback-prompt-handler"; +import { createDefaultFeedbackPromptHandler } from "./default-feedback-prompt-handler"; import * as feedbackLib from "./feedback"; import { FeedbackPromptCompletionHandler, @@ -146,7 +146,7 @@ export default function main() { feedbackPromptHandler = options.feedback?.liveFeedbackHandler ?? options.feedback?.liveSatisfactionHandler ?? - defaultFeedbackPromptHandler; + createDefaultFeedbackPromptHandler(options.feedback?.ui); log(`initialized with key "${trackingKey}" and options`, options); } diff --git a/src/types.ts b/src/types.ts index 630856a2..0f09b065 100644 --- a/src/types.ts +++ b/src/types.ts @@ -159,14 +159,15 @@ export type FeedbackPromptReplyHandler = ( reply: T, ) => T extends null ? Promise : Promise<{ feedbackId: string }>; +export type FeedbackPromptHandlerOpenFeedbackFormOptions = Omit< + RequestFeedbackOptions, + "featureId" | "userId" | "companyId" | "onClose" | "onDismiss" +>; + export type FeedbackPromptHandlerCallbacks = { reply: FeedbackPromptReplyHandler; - // dismiss: function, openFeedbackForm: ( - options: Omit< - RequestFeedbackOptions, - "featureId" | "userId" | "companyId" | "onClose" | "onDismiss" - >, + options: FeedbackPromptHandlerOpenFeedbackFormOptions, ) => void; }; diff --git a/test/e2e/feedback-widget.browser.spec.ts b/test/e2e/feedback-widget.browser.spec.ts index 8c021f46..64c583d2 100644 --- a/test/e2e/feedback-widget.browser.spec.ts +++ b/test/e2e/feedback-widget.browser.spec.ts @@ -6,10 +6,15 @@ import { feedbackContainerId, propagatedEvents, } from "../../src/feedback/constants"; +import { FeedbackTranslations } from "../../src/feedback/types"; +import type { Options } from "../../src/types"; const KEY = randomUUID(); const TRACKING_HOST = `https://tracking.bucket.co`; +const WINDOW_WIDTH = 1280; +const WINDOW_HEIGHT = 720; + declare global { interface Window { eventsFired: Record; @@ -20,7 +25,7 @@ function pick(options: T[]): T { return options[Math.floor(Math.random() * options.length)]; } -async function getOpenedWidgetContainer(page: Page) { +async function getOpenedWidgetContainer(page: Page, initOptions: Options = {}) { // Mock API calls await page.route(`${TRACKING_HOST}/${KEY}/user`, async (route) => { await route.fulfill({ status: 200 }); @@ -34,7 +39,7 @@ async function getOpenedWidgetContainer(page: Page) { // Golden path requests await page.evaluate(` ;(async () => { - bucket.init("${KEY}", {}); + bucket.init("${KEY}", ${JSON.stringify(initOptions)}); await bucket.user('foo') await bucket.requestFeedback({ featureId: "featureId1", @@ -96,6 +101,74 @@ test("Opens a feedback widget", async ({ page }) => { await expect(container.locator("dialog")).toHaveAttribute("open", ""); }); +test("Opens a feedback widget in the bottom right by default", async ({ + page, +}) => { + const container = await getOpenedWidgetContainer(page); + + await expect(container).toBeAttached(); + + const bbox = await container.locator("dialog").boundingBox(); + expect(bbox?.x).toEqual(WINDOW_WIDTH - bbox!.width - 16); + expect(bbox?.y).toBeGreaterThan(WINDOW_HEIGHT - bbox!.height - 16); // Account for browser differences + expect(bbox?.y).toBeLessThan(WINDOW_HEIGHT - bbox!.height); +}); + +test("Opens a feedback widget in the correct position when overridden", async ({ + page, +}) => { + const container = await getOpenedWidgetContainer(page, { + feedback: { + ui: { + position: { + type: "DIALOG", + placement: "top-left", + }, + }, + }, + }); + + await expect(container).toBeAttached(); + + const bbox = await container.locator("dialog").boundingBox(); + expect(bbox?.x).toEqual(16); + expect(bbox?.y).toBeGreaterThan(0); // Account for browser differences + expect(bbox?.y).toBeLessThan(16); +}); + +test("Opens a feedback widget with the correct translations", async ({ + page, +}) => { + const translations: Partial = { + ScoreStatusDescription: "Choisissez une note et laissez un commentaire", + ScoreVeryDissatisfiedLabel: "Très insatisfait", + ScoreDissatisfiedLabel: "Insatisfait", + ScoreNeutralLabel: "Neutre", + ScoreSatisfiedLabel: "Satisfait", + ScoreVerySatisfiedLabel: "Très satisfait", + SendButton: "Envoyer", + }; + + const container = await getOpenedWidgetContainer(page, { + feedback: { + ui: { + translations, + }, + }, + }); + + await expect(container).toBeAttached(); + await expect(container).toContainText(translations.ScoreStatusDescription!); + await expect(container).toContainText( + translations.ScoreVeryDissatisfiedLabel!, + ); + await expect(container).toContainText(translations.ScoreDissatisfiedLabel!); + await expect(container).toContainText(translations.ScoreNeutralLabel!); + await expect(container).toContainText(translations.ScoreSatisfiedLabel!); + await expect(container).toContainText(translations.ScoreVerySatisfiedLabel!); + await expect(container).toContainText(translations.SendButton!); +}); + test("Sends a request when choosing a score immediately", async ({ page }) => { const expectedScore = pick([1, 2, 3, 4, 5]); let sentJSON: object | null = null; From 65dc3e6a4c12ce5ec98d39dc5f4fe2d14814e3de Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Fri, 17 Nov 2023 15:30:16 +0100 Subject: [PATCH 04/14] 2.1.4 (#80) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index be818ac4..3457fbce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bucketco/tracking-sdk", - "version": "2.1.3", + "version": "2.1.4", "license": "MIT", "private": false, "repository": { From 1b7bed05acd42b190f2af4f4fcaa3f5e41cbc278 Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Fri, 17 Nov 2023 15:35:03 +0100 Subject: [PATCH 05/14] 2.1.5 (#83) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3457fbce..98a5c012 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bucketco/tracking-sdk", - "version": "2.1.4", + "version": "2.1.5", "license": "MIT", "private": false, "repository": { From 852c6754658870466d1f994313f8caf29688bba0 Mon Sep 17 00:00:00 2001 From: Richard Foster Date: Fri, 17 Nov 2023 15:17:47 +0000 Subject: [PATCH 06/14] Feedback Source (#78) * Include feedback source when submitting * Lowercase source names * Remove irrelevant source options from types * Improve documentation of source --- src/main.ts | 5 +++++ src/types.ts | 8 ++++++++ test/e2e/acceptance.node.test.ts | 1 + test/e2e/feedback-widget.browser.spec.ts | 3 +++ test/usage.test.ts | 2 ++ 5 files changed, 19 insertions(+) diff --git a/src/main.ts b/src/main.ts index de009ac3..b3423fbc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -231,6 +231,7 @@ export default function main() { comment, promptId, promptedQuestion, + source, }: Feedback) { checkKey(); if (!featureId) err("No featureId provided"); @@ -247,6 +248,7 @@ export default function main() { promptId, question, promptedQuestion, + source: source ?? "sdk", }; const res = await request(`${getUrl()}/feedback`, payload); @@ -383,6 +385,7 @@ export default function main() { promptId: message.promptId, question: reply.question, promptedQuestion: message.question, + source: "prompt", }); completionHandler(); @@ -470,6 +473,7 @@ export default function main() { featureId: options.featureId, userId: options.userId, companyId: options.companyId, + source: "widget", ...data, }); @@ -482,6 +486,7 @@ export default function main() { featureId: options.featureId, userId: options.userId, companyId: options.companyId, + source: "widget", ...data, }); diff --git a/src/types.ts b/src/types.ts index 0f09b065..bb34e04b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -138,6 +138,14 @@ export type Feedback = { * feedback. */ promptId?: FeedbackPrompt["promptId"]; + + /** + * Source of the feedback, depending on how the user was asked + * - `prompt` - Feedback submitted by a Live Satisfaction prompt + * - `widget` - Feedback submitted via `requestFeedback` + * - `sdk` - Feedback submitted via `feedback` + */ + source?: "prompt" | "sdk" | "widget"; }; export type FeedbackPrompt = { diff --git a/test/e2e/acceptance.node.test.ts b/test/e2e/acceptance.node.test.ts index 23cd6cba..f52ed9bf 100644 --- a/test/e2e/acceptance.node.test.ts +++ b/test/e2e/acceptance.node.test.ts @@ -44,6 +44,7 @@ test("Acceptance", async () => { featureId: "featureId1", score: 5, comment: "test!", + source: "sdk", }) .reply(200); diff --git a/test/e2e/feedback-widget.browser.spec.ts b/test/e2e/feedback-widget.browser.spec.ts index 64c583d2..08ec9398 100644 --- a/test/e2e/feedback-widget.browser.spec.ts +++ b/test/e2e/feedback-widget.browser.spec.ts @@ -193,6 +193,7 @@ test("Sends a request when choosing a score immediately", async ({ page }) => { score: expectedScore, question: "baz", userId: "foo", + source: "widget", }); }); @@ -251,6 +252,7 @@ test("Updates the score on every change", async ({ page }) => { question: "baz", score: 3, userId: "foo", + source: "widget", }); }); @@ -309,6 +311,7 @@ test("Sends a request with both the score and comment when submitting", async ({ featureId: "featureId1", feedbackId: "123", userId: "foo", + source: "widget", }); }); diff --git a/test/usage.test.ts b/test/usage.test.ts index 674d94cb..04a9c911 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -76,6 +76,7 @@ describe("usage", () => { comment: "Sunt bine!", question: "Cum esti?", promptedQuestion: "How are you?", + source: "sdk", }) .reply(200); @@ -683,6 +684,7 @@ describe("feedback state management", () => { score: 5, question: "Cum esti?", promptedQuestion: "How are you?", + source: "prompt", }) .reply(200, { feedbackId: "feedback123", From 5425c6f85c6542aeee242b4ed284addbadf1eb7e Mon Sep 17 00:00:00 2001 From: Richard Foster Date: Fri, 17 Nov 2023 15:18:51 +0000 Subject: [PATCH 07/14] 2.1.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98a5c012..3c29c6a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bucketco/tracking-sdk", - "version": "2.1.5", + "version": "2.1.6", "license": "MIT", "private": false, "repository": { From 7416ac15c03cfce60f999cd8ec477a4123d72de3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:24:35 +0000 Subject: [PATCH 08/14] Bump vite from 4.4.9 to 4.4.12 (#84) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.4.9 to 4.4.12. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v4.4.12/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v4.4.12/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 3c29c6a1..33659f71 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "ts-loader": "^9.4.4", "ts-node": "^10.9.1", "typescript": "^5.1.6", - "vite": "^4.4.9", + "vite": "^4.4.12", "vite-plugin-dts": "^3.5.2", "vitest": "^0.33.0", "webpack": "^5.88.2", diff --git a/yarn.lock b/yarn.lock index caf91fc3..8c536055 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4881,10 +4881,10 @@ vite-plugin-dts@^3.5.2: kolorist "^1.8.0" vue-tsc "^1.8.8" -"vite@^3.0.0 || ^4.0.0", vite@^4.4.9: - version "4.4.9" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.9.tgz#1402423f1a2f8d66fd8d15e351127c7236d29d3d" - integrity sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA== +"vite@^3.0.0 || ^4.0.0", vite@^4.4.12: + version "4.4.12" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.12.tgz#e9c355d5a0d8a47afa46cb4bad10820da333da5c" + integrity sha512-KtPlUbWfxzGVul8Nut8Gw2Qe8sBzWY+8QVc5SL8iRFnpnrcoCaNlzO40c1R6hPmcdTwIPEDkq0Y9+27a5tVbdQ== dependencies: esbuild "^0.18.10" postcss "^8.4.27" From dbc18b5d571a3a622ebc411a2d985214154cc939 Mon Sep 17 00:00:00 2001 From: Richard Foster Date: Thu, 7 Dec 2023 14:40:04 +0000 Subject: [PATCH 09/14] Improve documentation of CSS variables (#86) Co-authored-by: Alexandru Ciobanu --- FEEDBACK.md | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/FEEDBACK.md b/FEEDBACK.md index 8543ccf2..f7e36c6b 100644 --- a/FEEDBACK.md +++ b/FEEDBACK.md @@ -36,7 +36,7 @@ See also: - [Positioning and behavior](#positioning-and-behavior) for the position option. - [Static language configuration](#static-language-configuration) if you want to translate the feedback UI. -- [Live Satisfaction](#live-feedback) to override default configuration. +- [Live Satisfaction](#live-satisfaction) to override default configuration. ## Live Satisfaction @@ -289,9 +289,39 @@ bucket.init("bucket-tracking-key", { You can adapt parts of the look of the Bucket feedback UI by applying CSS custom properties to your page in your CSS `:root`-scope. -![image](https://github.com/bucketco/bucket-tracking-sdk/assets/331790/ff7ed885-8308-4c9b-98c6-5623f1026b69) +For example, a dark mode theme might look like this: + +image + +```css +:root { + --bucket-feedback-dialog-background-color: #1e1f24; + --bucket-feedback-dialog-color: rgba(255, 255, 255, 0.92); + --bucket-feedback-dialog-secondary-color: rgba(255, 255, 255, 0.3); + --bucket-feedback-dialog-border: rgba(255, 255, 255, 0.16); + --bucket-feedback-dialog-primary-button-background-color: #655bfa; + --bucket-feedback-dialog-primary-button-color: white; + --bucket-feedback-dialog-input-border-color: rgba(255, 255, 255, 0.16); + --bucket-feedback-dialog-input-focus-border-color: rgba(255, 255, 255, 0.3); + --bucket-feedback-dialog-error-color: #f56565; + + --bucket-feedback-dialog-rating-1-color: #ed8936; + --bucket-feedback-dialog-rating-1-background-color: #7b341e; + --bucket-feedback-dialog-rating-2-color: #dd6b20; + --bucket-feedback-dialog-rating-2-background-color: #652b19; + --bucket-feedback-dialog-rating-3-color: #787c91; + --bucket-feedback-dialog-rating-3-background-color: #3e404c; + --bucket-feedback-dialog-rating-4-color: #38a169; + --bucket-feedback-dialog-rating-4-background-color: #1c4532; + --bucket-feedback-dialog-rating-5-color: #48bb78; + --bucket-feedback-dialog-rating-5-background-color: #22543d; + + --bucket-feedback-dialog-submitted-check-background-color: #38a169; + --bucket-feedback-dialog-submitted-check-color: #ffffff; +} +``` -Examples of custom styling can be found in our [development example stylesheet](./dev/index.css). +Other examples of custom styling can be found in our [development example stylesheet](./dev/index.css). ## Using your own UI to collect feedback From 25efeacc185ebd8820cf02ad7f533426e867a7c1 Mon Sep 17 00:00:00 2001 From: Richard Foster Date: Fri, 8 Dec 2023 12:17:46 +0000 Subject: [PATCH 10/14] Add JSDoc documentation to SDK methods (#85) * Add JSDoc documentation to SDK methods * Capitalisation --- src/main.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 15 ++++++++++++-- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/main.ts b/src/main.ts index b3423fbc..978afd02 100644 --- a/src/main.ts +++ b/src/main.ts @@ -103,6 +103,14 @@ export default function main() { return userId!; } + /** + * Initialize the Bucket SDK. + * + * Must be called before calling other SDK methods. + * + * @param key Your Bucket tracking key + * @param options + */ function init(key: Key, options: Options = {}) { reset(); if (!key) { @@ -151,6 +159,13 @@ export default function main() { log(`initialized with key "${trackingKey}" and options`, options); } + /** + * Identify the current user in Bucket, so they are tracked against each `track` call and receive Live Satisfaction events. + * + * @param userId The ID you use to identify this user + * @param attributes Any attributes you want to attach to the user in Bucket + * @param context + */ async function user( userId: User["userId"], attributes?: User["attributes"], @@ -177,6 +192,14 @@ export default function main() { return res; } + /** + * Identify the current user's company in Bucket, so it is tracked against each `track` call. + * + * @param companyId The ID you use to identify this company + * @param attributes Any attributes you want to attach to the company in Bucket + * @param userId The ID you use to identify the current user + * @param context + */ async function company( companyId: Company["companyId"], attributes?: Company["attributes"] | null, @@ -198,6 +221,15 @@ export default function main() { return res; } + /** + * Track an event in Bucket. + * + * @param eventName The name of the event + * @param attributes Any attributes you want to attach to the event + * @param userId The ID you use to identify the current user + * @param companyId The ID you use to identify the current user's company + * @param context + */ async function track( eventName: TrackedEvent["event"], attributes?: TrackedEvent["attributes"] | null, @@ -221,6 +253,12 @@ export default function main() { return res; } + /** + * Submit user feedback to Bucket. Must include either `score` or `comment`, or both. + * + * @param options + * @returns + */ async function feedback({ feedbackId, featureId, @@ -260,6 +298,14 @@ export default function main() { * @deprecated Use `initLiveSatisfaction` instead */ const initLiveFeedback = initLiveSatisfaction; + + /** + * Start receiving Live Satisfaction feedback prompts. + * + * This doesn't need to be called unless you set `enableLiveSatisfaction` to false when calling `init`. + * + * @param userId The ID you use to identify the user + */ async function initLiveSatisfaction(userId?: User["userId"]) { checkKey(); @@ -442,6 +488,13 @@ export default function main() { return res; } + /** + * Display the Bucket feedback form UI programmatically. + * + * This can be used to collect feedback from users in Bucket in cases where Live Satisfaction isn't appropriate. + * + * @param options + */ function requestFeedback(options: RequestFeedbackOptions) { if (isForNode) { err("requestFeedback can only be called in the browser"); @@ -496,6 +549,9 @@ export default function main() { }, 1); } + /** + * Reset the active user and disconnect from Live Satisfaction events. + */ function reset() { sessionUserId = undefined; feedbackPromptingUserId = undefined; diff --git a/src/types.ts b/src/types.ts index bb34e04b..ba19fdb9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -69,8 +69,19 @@ export type TrackedEvent = { export interface RequestFeedbackOptions extends Omit { + /** + * Bucket feature ID + */ featureId: string; + + /** + * User ID from your own application. + */ userId: string; + + /** + * Company ID from your own application. + */ companyId?: string; /** @@ -98,12 +109,12 @@ export type Feedback = { featureId: string; /** - * User id from your own application. + * User ID from your own application. */ userId?: User["userId"]; /** - * Company id from your own application. + * Company ID from your own application. */ companyId?: Company["companyId"]; From f626a3cdae762c0debc1ca2d15e215d2280ecb7e Mon Sep 17 00:00:00 2001 From: Richard Foster Date: Fri, 8 Dec 2023 12:18:35 +0000 Subject: [PATCH 11/14] 2.1.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 33659f71..1be8ad88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bucketco/tracking-sdk", - "version": "2.1.6", + "version": "2.1.7", "license": "MIT", "private": false, "repository": { From 2b4de3be03f0d31254cb4a854ddd3cc9fc469732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20M=C3=BCller?= Date: Mon, 11 Dec 2023 08:53:00 +0100 Subject: [PATCH 12/14] Correct wrong note about 'unsafe-inline'. style directive --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 229a404e..7f184a02 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ If you are running with strict Content Security Policies active on your website, | ----------- | ------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | connect-src | https://tracking.bucket.co | tracking | Used for all tracking methods: `bucket.user()`, `bucket.company()`, `bucket.track()` and `bucket.feedback()` | | connect-src | https://livemessaging.bucket.co | live satisfaction | Server sent events from the Bucket Live Satisfaction service, which allows for automatically collecting feedback when a user used a feature. | -| style-src | 'unsafe-inline' | feedback UI | The feedback UI is styled with inline script tags. Not having this directive results unstyled HTML elements. | +| style-src | 'unsafe-inline' | feedback UI | The feedback UI is styled with inline styles. Not having this directive results unstyled HTML elements. | If you are including the Bucket tracking SDK with a `