@sessionsight/feedback 1.0.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SessionSight
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # @sessionsight/feedback
2
+
3
+ Collect structured feedback, bug reports, and ratings from your users.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @sessionsight/feedback
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import SessionSightFeedback from '@sessionsight/feedback';
15
+
16
+ SessionSightFeedback.init({
17
+ secretApiKey: 'sessionsight_sec_...',
18
+ propertyId: 'your-property-id',
19
+ });
20
+
21
+ // Submit feedback
22
+ await SessionSightFeedback.submit('bug-report', {
23
+ userId: 'user_456',
24
+ option: 'critical',
25
+ message: 'The checkout page crashes on Safari',
26
+ metadata: { page: '/checkout', browser: 'Safari 17' },
27
+ });
28
+ ```
29
+
30
+ ## Configuration
31
+
32
+ | Option | Type | Default | Description |
33
+ |--------|------|---------|-------------|
34
+ | `secretApiKey` | `string` | **required** | Your secret API key from the dashboard |
35
+ | `propertyId` | `string` | **required** | Your property ID |
36
+ | `apiUrl` | `string` | `https://api.sessionsight.com` | Override for self-hosted or local dev |
37
+
38
+ ## API
39
+
40
+ ### `SessionSightFeedback.init(config)`
41
+
42
+ Initialize the SDK with your credentials.
43
+
44
+ ### `SessionSightFeedback.submit(feedbackTypeId, options?)`
45
+
46
+ Submit feedback.
47
+
48
+ | Parameter | Type | Required | Description |
49
+ |-----------|------|----------|-------------|
50
+ | `feedbackTypeId` | `string` | Yes | The feedback type slug (created in the dashboard) |
51
+ | `options.userId` | `string` | No | Identifier for the user submitting the feedback |
52
+ | `options.visitorId` | `string` | No | SessionSight visitor ID for session attribution |
53
+ | `options.sessionId` | `string` | No | SessionSight session ID to link feedback to a specific session |
54
+ | `options.option` | `string` | No | Selected option value (must match a pre-defined option) |
55
+ | `options.message` | `string` | No | Free-text message (only if the feedback type allows text input) |
56
+ | `options.metadata` | `Record<string, string>` | No | Additional key-value context |
57
+
58
+ Returns `Promise<{ success: boolean; error?: string }>`.
59
+
60
+ ### `SessionSightFeedback.destroy()`
61
+
62
+ Clean up resources.
63
+
64
+ ## Class-based Usage
65
+
66
+ ```typescript
67
+ import { FeedbackClient } from '@sessionsight/feedback';
68
+
69
+ const feedback = new FeedbackClient({
70
+ secretApiKey: 'sessionsight_sec_...',
71
+ propertyId: 'your-property-id',
72
+ });
73
+
74
+ await feedback.submit('star-rating', { option: '5', userId: 'user_123' });
75
+
76
+ feedback.destroy();
77
+ ```
78
+
79
+ ## Use Cases
80
+
81
+ ### Bug Reports
82
+
83
+ ```typescript
84
+ await SessionSightFeedback.submit('bug-report', {
85
+ userId: 'user_123',
86
+ option: 'critical',
87
+ message: 'Payment fails with Amex cards',
88
+ metadata: { page: '/checkout', browser: 'Chrome 120' },
89
+ });
90
+ ```
91
+
92
+ ### Star Ratings (1-5)
93
+
94
+ ```typescript
95
+ await SessionSightFeedback.submit('star-rating', {
96
+ option: '4',
97
+ userId: 'user_123',
98
+ });
99
+ ```
100
+
101
+ ### Sentiment Feedback (Good/Neutral/Bad)
102
+
103
+ ```typescript
104
+ await SessionSightFeedback.submit('page-feedback', {
105
+ option: 'good',
106
+ userId: 'user_123',
107
+ message: 'This article was really helpful!',
108
+ });
109
+ ```
110
+
111
+ ## Script Tag
112
+
113
+ ```html
114
+ <script src="https://cdn.sessionsight.com/sessionsight-feedback.js"></script>
115
+ <script>
116
+ SessionSightFeedback.init({
117
+ secretApiKey: 'sessionsight_sec_...',
118
+ propertyId: 'your-property-id',
119
+ });
120
+ </script>
121
+ ```
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@sessionsight/feedback",
3
+ "version": "1.0.0",
4
+ "description": "User feedback and survey SDK for SessionSight.",
5
+ "author": "SessionSight",
6
+ "license": "MIT",
7
+ "homepage": "https://sessionsight.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/SessionSight/sdks.git",
11
+ "directory": "packages/feedback"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/SessionSight/sdks/issues"
15
+ },
16
+ "keywords": [
17
+ "feedback",
18
+ "surveys",
19
+ "user-feedback",
20
+ "sessionsight"
21
+ ],
22
+ "type": "module",
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "import": "./dist/index.js",
28
+ "types": "./dist/index.d.ts"
29
+ }
30
+ },
31
+ "scripts": {
32
+ "build": "bun build src/index.ts --outdir dist --format esm && bun x tsc --emitDeclarationOnly --declaration --outDir dist"
33
+ },
34
+ "peerDependencies": {
35
+ "typescript": "^5"
36
+ },
37
+ "dependencies": {
38
+ "@sessionsight/sdk-shared": "workspace:*"
39
+ }
40
+ }
package/src/client.ts ADDED
@@ -0,0 +1,64 @@
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 ADDED
@@ -0,0 +1,34 @@
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 ADDED
@@ -0,0 +1,19 @@
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
+ }
@@ -0,0 +1,150 @@
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 ADDED
@@ -0,0 +1,14 @@
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
+ }