@sessionsight/feedback 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ import type { FeedbackConfig, FeedbackOptions, FeedbackResult } from './types.js';
2
+ export declare class FeedbackClient {
3
+ private apiUrl;
4
+ private secretApiKey;
5
+ private propertyId;
6
+ constructor(config: FeedbackConfig);
7
+ submit(feedbackTypeId: string, options?: FeedbackOptions): Promise<FeedbackResult>;
8
+ destroy(): void;
9
+ }
package/dist/iife.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { FeedbackClient } from './client.js';
2
+ import type { FeedbackConfig, FeedbackOptions, FeedbackResult } from './types.js';
3
+ export { FeedbackClient };
4
+ export type { FeedbackConfig, FeedbackOptions, FeedbackResult } from './types.js';
5
+ declare const SessionSightFeedback: {
6
+ init(config: FeedbackConfig): void;
7
+ submit(feedbackTypeId: string, options?: FeedbackOptions): Promise<FeedbackResult>;
8
+ destroy(): void;
9
+ };
10
+ export default SessionSightFeedback;
package/dist/index.js ADDED
@@ -0,0 +1,177 @@
1
+ // ../sdk-shared/src/index.ts
2
+ var DEFAULT_API_URL = "https://api.sessionsight.com";
3
+ function normalizeApiUrl(url) {
4
+ const normalized = (url || DEFAULT_API_URL).replace(/\/$/, "");
5
+ if (normalized.startsWith("http://") && !normalized.startsWith("http://localhost")) {
6
+ console.warn("SessionSight: API URL uses http:// instead of https://. Data will be transmitted unencrypted.");
7
+ }
8
+ return normalized;
9
+ }
10
+ var VISITOR_STORAGE_KEY = "sessionsight_visitor_id";
11
+ var VISITOR_COOKIE_NAME = "ss_vid";
12
+ var VISITOR_COOKIE_MAX_AGE = 365 * 24 * 60 * 60;
13
+ function readCookie(name) {
14
+ if (typeof document === "undefined")
15
+ return null;
16
+ try {
17
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18
+ const match = document.cookie.match(new RegExp("(?:^|; )" + escaped + "=([^;]*)"));
19
+ return match ? decodeURIComponent(match[1]) : null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ function setCookie(name, value, maxAge) {
25
+ if (typeof document === "undefined")
26
+ return;
27
+ try {
28
+ const secure = location.protocol === "https:" ? "; Secure" : "";
29
+ document.cookie = `${name}=${value}; path=/; max-age=${maxAge}; SameSite=Lax${secure}`;
30
+ } catch {}
31
+ }
32
+ function generateUUID() {
33
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
34
+ return crypto.randomUUID();
35
+ }
36
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
37
+ const r = Math.random() * 16 | 0;
38
+ return (c === "x" ? r : r & 3 | 8).toString(16);
39
+ });
40
+ }
41
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
42
+ function isValidUUID(id) {
43
+ return UUID_RE.test(id);
44
+ }
45
+ function hasLocalStorage() {
46
+ try {
47
+ const key = "__ss_test__";
48
+ localStorage.setItem(key, "1");
49
+ localStorage.removeItem(key);
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+ function shouldSuppressPersistentId() {
56
+ if (typeof navigator !== "undefined") {
57
+ if (navigator.globalPrivacyControl === true)
58
+ return true;
59
+ if (navigator.doNotTrack === "1")
60
+ return true;
61
+ }
62
+ return false;
63
+ }
64
+ function getOrCreateVisitorId(providedId) {
65
+ if (providedId)
66
+ return providedId;
67
+ if (shouldSuppressPersistentId()) {
68
+ return generateUUID();
69
+ }
70
+ const canStore = typeof window !== "undefined" && hasLocalStorage();
71
+ const cookieId = readCookie(VISITOR_COOKIE_NAME);
72
+ if (cookieId && isValidUUID(cookieId)) {
73
+ if (canStore)
74
+ localStorage.setItem(VISITOR_STORAGE_KEY, cookieId);
75
+ return cookieId;
76
+ }
77
+ if (canStore) {
78
+ const stored = localStorage.getItem(VISITOR_STORAGE_KEY);
79
+ if (stored && isValidUUID(stored)) {
80
+ setCookie(VISITOR_COOKIE_NAME, stored, VISITOR_COOKIE_MAX_AGE);
81
+ return stored;
82
+ }
83
+ }
84
+ const id = generateUUID();
85
+ if (canStore)
86
+ localStorage.setItem(VISITOR_STORAGE_KEY, id);
87
+ setCookie(VISITOR_COOKIE_NAME, id, VISITOR_COOKIE_MAX_AGE);
88
+ return id;
89
+ }
90
+ var registry = new Map;
91
+
92
+ // src/client.ts
93
+ var FETCH_TIMEOUT_MS = 1e4;
94
+ function fetchWithTimeout(url, options) {
95
+ const controller = new AbortController;
96
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
97
+ return fetch(url, { ...options, signal: controller.signal }).finally(() => clearTimeout(timer));
98
+ }
99
+
100
+ class FeedbackClient {
101
+ apiUrl;
102
+ secretApiKey;
103
+ propertyId;
104
+ constructor(config) {
105
+ if (typeof window !== "undefined" && !("process" in globalThis)) {
106
+ throw new Error("@sessionsight/feedback is a server-side SDK and cannot be used in the browser.");
107
+ }
108
+ if (!config.secretApiKey?.trim())
109
+ throw new Error("@sessionsight/feedback: secretApiKey is required.");
110
+ if (!config.propertyId?.trim())
111
+ throw new Error("@sessionsight/feedback: propertyId is required.");
112
+ this.secretApiKey = config.secretApiKey;
113
+ this.propertyId = config.propertyId;
114
+ this.apiUrl = normalizeApiUrl(config.apiUrl || "");
115
+ }
116
+ async submit(feedbackTypeId, options) {
117
+ try {
118
+ const res = await fetchWithTimeout(`${this.apiUrl}/v1/feedback/submit`, {
119
+ method: "POST",
120
+ headers: {
121
+ "Content-Type": "application/json",
122
+ "x-api-key": this.secretApiKey
123
+ },
124
+ body: JSON.stringify({
125
+ feedbackTypeId,
126
+ propertyId: this.propertyId,
127
+ visitorId: options?.visitorId || getOrCreateVisitorId(),
128
+ ...options?.userId ? { userId: options.userId } : {},
129
+ ...options?.sessionId ? { sessionId: options.sessionId } : {},
130
+ ...options?.option ? { option: options.option } : {},
131
+ ...options?.message ? { message: options.message } : {},
132
+ ...options?.metadata ? { metadata: options.metadata } : {}
133
+ })
134
+ });
135
+ if (!res.ok) {
136
+ const data = await res.json().catch(() => ({}));
137
+ return { success: false, error: data.error || `HTTP ${res.status}` };
138
+ }
139
+ return { success: true };
140
+ } catch (err) {
141
+ const message = err instanceof Error ? err.message : "Unknown error";
142
+ console.warn("[SessionSight Feedback] Failed to submit feedback:", message);
143
+ return { success: false, error: message };
144
+ }
145
+ }
146
+ destroy() {}
147
+ }
148
+
149
+ // src/index.ts
150
+ var instance = null;
151
+ var SessionSightFeedback = {
152
+ init(config) {
153
+ if (instance) {
154
+ console.warn("[SessionSight Feedback] Already initialized. Call destroy() first.");
155
+ return;
156
+ }
157
+ instance = new FeedbackClient(config);
158
+ },
159
+ async submit(feedbackTypeId, options) {
160
+ if (!instance) {
161
+ console.warn("[SessionSight Feedback] Not initialized. Call init() first.");
162
+ return { success: false, error: "Not initialized" };
163
+ }
164
+ return instance.submit(feedbackTypeId, options);
165
+ },
166
+ destroy() {
167
+ if (instance) {
168
+ instance.destroy();
169
+ instance = null;
170
+ }
171
+ }
172
+ };
173
+ var src_default = SessionSightFeedback;
174
+ export {
175
+ src_default as default,
176
+ FeedbackClient
177
+ };
@@ -0,0 +1 @@
1
+ (()=>{class d{apiUrl;secretApiKey;propertyId;constructor(t){this.secretApiKey=t.secretApiKey,this.propertyId=t.propertyId,this.apiUrl=(t.apiUrl||"https://api.sessionsight.com").replace(/\/$/,"")}async submit(t,e){try{let a=await fetch(`${this.apiUrl}/v1/feedback/submit`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.secretApiKey},body:JSON.stringify({feedbackTypeId:t,propertyId:this.propertyId,...e?.userId?{userId:e.userId}:{},...e?.option?{option:e.option}:{},...e?.message?{message:e.message}:{},...e?.metadata?{metadata:e.metadata}:{}})});if(!a.ok)return{success:!1,error:(await a.json().catch(()=>({}))).error||`HTTP ${a.status}`};return{success:!0}}catch(a){let i=a instanceof Error?a.message:"Unknown error";return console.warn("[SessionSight Feedback] Failed to submit feedback:",i),{success:!1,error:i}}}destroy(){}}var l=null,F={init(t){l=new d(t)},async submit(t,e){if(!l)return console.warn("[SessionSight Feedback] Not initialized. Call init() first."),{success:!1,error:"Not initialized"};return l.submit(t,e)},destroy(){if(l)l.destroy(),l=null}},b=F;globalThis.SessionSightFeedback=b;})();
@@ -0,0 +1,17 @@
1
+ export interface FeedbackConfig {
2
+ secretApiKey: string;
3
+ propertyId: string;
4
+ apiUrl?: string;
5
+ }
6
+ export interface FeedbackOptions {
7
+ userId?: string;
8
+ visitorId?: string;
9
+ sessionId?: string;
10
+ option?: string;
11
+ message?: string;
12
+ metadata?: Record<string, string>;
13
+ }
14
+ export interface FeedbackResult {
15
+ success: boolean;
16
+ error?: string;
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sessionsight/feedback",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "User feedback and survey SDK for SessionSight.",
5
5
  "author": "SessionSight",
6
6
  "license": "MIT",
@@ -19,6 +19,9 @@
19
19
  "user-feedback",
20
20
  "sessionsight"
21
21
  ],
22
+ "files": [
23
+ "dist"
24
+ ],
22
25
  "type": "module",
23
26
  "main": "./dist/index.js",
24
27
  "types": "./dist/index.d.ts",
package/src/client.ts DELETED
@@ -1,64 +0,0 @@
1
- import type { FeedbackConfig, FeedbackOptions, FeedbackResult } from './types.js';
2
- import { normalizeApiUrl, getOrCreateVisitorId } from '@sessionsight/sdk-shared';
3
-
4
- const FETCH_TIMEOUT_MS = 10_000;
5
-
6
- function fetchWithTimeout(url: string, options: RequestInit): Promise<Response> {
7
- const controller = new AbortController();
8
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
9
- return fetch(url, { ...options, signal: controller.signal }).finally(() => clearTimeout(timer));
10
- }
11
-
12
- export class FeedbackClient {
13
- private apiUrl: string;
14
- private secretApiKey: string;
15
- private propertyId: string;
16
-
17
- constructor(config: FeedbackConfig) {
18
- if (typeof window !== 'undefined' && !('process' in globalThis)) {
19
- throw new Error('@sessionsight/feedback is a server-side SDK and cannot be used in the browser.');
20
- }
21
- if (!config.secretApiKey?.trim()) throw new Error('@sessionsight/feedback: secretApiKey is required.');
22
- if (!config.propertyId?.trim()) throw new Error('@sessionsight/feedback: propertyId is required.');
23
- this.secretApiKey = config.secretApiKey;
24
- this.propertyId = config.propertyId;
25
- this.apiUrl = normalizeApiUrl(config.apiUrl || '');
26
- }
27
-
28
- async submit(feedbackTypeId: string, options?: FeedbackOptions): Promise<FeedbackResult> {
29
- try {
30
- const res = await fetchWithTimeout(`${this.apiUrl}/v1/feedback/submit`, {
31
- method: 'POST',
32
- headers: {
33
- 'Content-Type': 'application/json',
34
- 'x-api-key': this.secretApiKey,
35
- },
36
- body: JSON.stringify({
37
- feedbackTypeId,
38
- propertyId: this.propertyId,
39
- visitorId: options?.visitorId || getOrCreateVisitorId(),
40
- ...(options?.userId ? { userId: options.userId } : {}),
41
- ...(options?.sessionId ? { sessionId: options.sessionId } : {}),
42
- ...(options?.option ? { option: options.option } : {}),
43
- ...(options?.message ? { message: options.message } : {}),
44
- ...(options?.metadata ? { metadata: options.metadata } : {}),
45
- }),
46
- });
47
-
48
- if (!res.ok) {
49
- const data = await res.json().catch(() => ({}));
50
- return { success: false, error: (data as any).error || `HTTP ${res.status}` };
51
- }
52
-
53
- return { success: true };
54
- } catch (err) {
55
- const message = err instanceof Error ? err.message : 'Unknown error';
56
- console.warn('[SessionSight Feedback] Failed to submit feedback:', message);
57
- return { success: false, error: message };
58
- }
59
- }
60
-
61
- destroy(): void {
62
- // No-op for now; reserved for future cleanup (e.g., batching, intervals)
63
- }
64
- }
package/src/index.ts DELETED
@@ -1,34 +0,0 @@
1
- import { FeedbackClient } from './client.js';
2
- import type { FeedbackConfig, FeedbackOptions, FeedbackResult } from './types.js';
3
-
4
- export { FeedbackClient };
5
- export type { FeedbackConfig, FeedbackOptions, FeedbackResult } from './types.js';
6
-
7
- let instance: FeedbackClient | null = null;
8
-
9
- const SessionSightFeedback = {
10
- init(config: FeedbackConfig): void {
11
- if (instance) {
12
- console.warn('[SessionSight Feedback] Already initialized. Call destroy() first.');
13
- return;
14
- }
15
- instance = new FeedbackClient(config);
16
- },
17
-
18
- async submit(feedbackTypeId: string, options?: FeedbackOptions): Promise<FeedbackResult> {
19
- if (!instance) {
20
- console.warn('[SessionSight Feedback] Not initialized. Call init() first.');
21
- return { success: false, error: 'Not initialized' };
22
- }
23
- return instance.submit(feedbackTypeId, options);
24
- },
25
-
26
- destroy(): void {
27
- if (instance) {
28
- instance.destroy();
29
- instance = null;
30
- }
31
- },
32
- };
33
-
34
- export default SessionSightFeedback;
package/src/types.ts DELETED
@@ -1,19 +0,0 @@
1
- export interface FeedbackConfig {
2
- secretApiKey: string;
3
- propertyId: string;
4
- apiUrl?: string;
5
- }
6
-
7
- export interface FeedbackOptions {
8
- userId?: string;
9
- visitorId?: string;
10
- sessionId?: string;
11
- option?: string;
12
- message?: string;
13
- metadata?: Record<string, string>;
14
- }
15
-
16
- export interface FeedbackResult {
17
- success: boolean;
18
- error?: string;
19
- }
@@ -1,150 +0,0 @@
1
- import { test, expect, mock, beforeEach } from 'bun:test';
2
- import { FeedbackClient } from '../src/client.js';
3
-
4
- const originalFetch = globalThis.fetch;
5
-
6
- beforeEach(() => {
7
- globalThis.fetch = originalFetch;
8
- });
9
-
10
- test('submit sends correct request', async () => {
11
- const mockFetch = mock(() =>
12
- Promise.resolve(new Response(JSON.stringify({ success: true }), { status: 200 }))
13
- );
14
- globalThis.fetch = mockFetch;
15
-
16
- const client = new FeedbackClient({
17
- secretApiKey: 'sk_test_123',
18
- propertyId: 'prop-1',
19
- apiUrl: 'https://api.example.com',
20
- });
21
-
22
- const result = await client.submit('bug-report', {
23
- userId: 'user_456',
24
- option: 'critical',
25
- message: 'Page crashes on Safari',
26
- metadata: { page: '/checkout' },
27
- });
28
-
29
- expect(result).toEqual({ success: true });
30
- expect(mockFetch).toHaveBeenCalledTimes(1);
31
-
32
- const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit];
33
- expect(url).toBe('https://api.example.com/v1/feedback/submit');
34
- expect(opts.method).toBe('POST');
35
- expect(opts.headers).toEqual({
36
- 'Content-Type': 'application/json',
37
- 'x-api-key': 'sk_test_123',
38
- });
39
- const body = JSON.parse(opts.body as string);
40
- expect(body.feedbackTypeId).toBe('bug-report');
41
- expect(body.propertyId).toBe('prop-1');
42
- expect(body.userId).toBe('user_456');
43
- expect(body.option).toBe('critical');
44
- expect(body.message).toBe('Page crashes on Safari');
45
- expect(body.metadata).toEqual({ page: '/checkout' });
46
- });
47
-
48
- test('submit with no options sends minimal payload', async () => {
49
- const mockFetch = mock(() =>
50
- Promise.resolve(new Response(JSON.stringify({ success: true }), { status: 200 }))
51
- );
52
- globalThis.fetch = mockFetch;
53
-
54
- const client = new FeedbackClient({
55
- secretApiKey: 'sk_test_123',
56
- propertyId: 'prop-1',
57
- });
58
-
59
- await client.submit('general');
60
-
61
- const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
62
- expect(body.feedbackTypeId).toBe('general');
63
- expect(body.propertyId).toBe('prop-1');
64
- expect(body.userId).toBeUndefined();
65
- expect(body.option).toBeUndefined();
66
- expect(body.message).toBeUndefined();
67
- expect(body.metadata).toBeUndefined();
68
- });
69
-
70
- test('submit returns error on non-ok response', async () => {
71
- globalThis.fetch = mock(() =>
72
- Promise.resolve(new Response(JSON.stringify({ error: 'Feedback type not found' }), { status: 404 }))
73
- );
74
-
75
- const client = new FeedbackClient({
76
- secretApiKey: 'sk_test_123',
77
- propertyId: 'prop-1',
78
- });
79
-
80
- const result = await client.submit('nonexistent');
81
- expect(result.success).toBe(false);
82
- expect(result.error).toBe('Feedback type not found');
83
- });
84
-
85
- test('submit returns error on network failure', async () => {
86
- globalThis.fetch = mock(() => Promise.reject(new Error('Network error')));
87
-
88
- const client = new FeedbackClient({
89
- secretApiKey: 'sk_test_123',
90
- propertyId: 'prop-1',
91
- });
92
-
93
- const result = await client.submit('bug-report');
94
- expect(result.success).toBe(false);
95
- expect(result.error).toBe('Network error');
96
- });
97
-
98
- test('strips trailing slash from apiUrl', async () => {
99
- const mockFetch = mock(() =>
100
- Promise.resolve(new Response(JSON.stringify({ success: true }), { status: 200 }))
101
- );
102
- globalThis.fetch = mockFetch;
103
-
104
- const client = new FeedbackClient({
105
- secretApiKey: 'sk_test_123',
106
- propertyId: 'prop-1',
107
- apiUrl: 'https://api.example.com/',
108
- });
109
-
110
- await client.submit('test');
111
- const url = (mockFetch.mock.calls[0] as [string, RequestInit])[0];
112
- expect(url).toBe('https://api.example.com/v1/feedback/submit');
113
- });
114
-
115
- test('uses default apiUrl when not provided', async () => {
116
- const mockFetch = mock(() =>
117
- Promise.resolve(new Response(JSON.stringify({ success: true }), { status: 200 }))
118
- );
119
- globalThis.fetch = mockFetch;
120
-
121
- const client = new FeedbackClient({
122
- secretApiKey: 'sk_test_123',
123
- propertyId: 'prop-1',
124
- });
125
-
126
- await client.submit('test');
127
- const url = (mockFetch.mock.calls[0] as [string, RequestInit])[0];
128
- expect(url).toBe('https://api.sessionsight.com/v1/feedback/submit');
129
- });
130
-
131
- test('submit only includes provided optional fields', async () => {
132
- const mockFetch = mock(() =>
133
- Promise.resolve(new Response(JSON.stringify({ success: true }), { status: 200 }))
134
- );
135
- globalThis.fetch = mockFetch;
136
-
137
- const client = new FeedbackClient({
138
- secretApiKey: 'sk_test_123',
139
- propertyId: 'prop-1',
140
- });
141
-
142
- await client.submit('star-rating', { option: '5' });
143
-
144
- const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
145
- expect(body.feedbackTypeId).toBe('star-rating');
146
- expect(body.option).toBe('5');
147
- expect(body.userId).toBeUndefined();
148
- expect(body.message).toBeUndefined();
149
- expect(body.metadata).toBeUndefined();
150
- });
package/tsconfig.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "declaration": true,
6
- "noEmit": false,
7
- "rootDir": "./src",
8
- "lib": ["ES2022", "DOM"],
9
- "module": "ESNext",
10
- "moduleResolution": "bundler"
11
- },
12
- "include": ["src/**/*.ts"],
13
- "exclude": ["src/**/*.test.ts"]
14
- }