@hellsoft/vaka-node 0.2.0 → 0.3.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/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ FeedbackError: () => FeedbackError,
33
34
  SDK: () => SDK,
34
35
  VakaScopeSpanProcessor: () => VakaScopeSpanProcessor,
35
36
  _globalScope: () => globalScope,
@@ -37,6 +38,7 @@ __export(index_exports, {
37
38
  addBreadcrumb: () => addBreadcrumb,
38
39
  captureError: () => captureError,
39
40
  captureException: () => captureException,
41
+ captureFeedback: () => captureFeedback,
40
42
  captureMessage: () => captureMessage,
41
43
  close: () => close,
42
44
  currentScope: () => currentScope,
@@ -556,7 +558,7 @@ function makeFastifyPlugin(capture, shouldCapture2) {
556
558
  }
557
559
 
558
560
  // src/index.ts
559
- var SDK = { name: "vaka-node", version: "0.2.0" };
561
+ var SDK = { name: "vaka-node", version: "0.3.0" };
560
562
  var opts;
561
563
  var transport;
562
564
  var otelSdk;
@@ -751,6 +753,83 @@ function captureMessage(message, level = "info", ctx = {}) {
751
753
  const item = { type: "error", ...baseItem(currentScope(), ctx), level, message, fingerprint: ctx.fingerprint };
752
754
  return enqueue(item, level === "error" || level === "fatal") ? item.event_id : void 0;
753
755
  }
756
+ var FeedbackError = class extends Error {
757
+ /** HTTP status, or 0 when nothing was sent. 429 means a rate limit was hit. */
758
+ status;
759
+ constructor(message, status = 0) {
760
+ super(message);
761
+ this.name = "FeedbackError";
762
+ this.status = status;
763
+ }
764
+ };
765
+ function contentTypeOf(filename) {
766
+ const ext = filename.toLowerCase().split(".").pop();
767
+ return ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : "image/jpeg";
768
+ }
769
+ async function captureFeedback(f) {
770
+ if (!opts || !transport) throw new FeedbackError("vaka is not initialized");
771
+ const message = String(f?.message ?? "").trim();
772
+ if (!message) throw new FeedbackError("feedback needs a message");
773
+ const files = (f.attachments || []).filter(Boolean);
774
+ if (files.length > 3) throw new FeedbackError("at most 3 attachments per feedback");
775
+ const scope = currentScope();
776
+ const userId = f.userId || scope.user?.id;
777
+ const item = {
778
+ id: (0, import_node_crypto.randomUUID)(),
779
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
780
+ platform: "node",
781
+ source: "api",
782
+ message: message.slice(0, 5e3),
783
+ email: f.email?.trim() || void 0,
784
+ name: f.name?.trim() || void 0,
785
+ event_id: f.eventId,
786
+ issue: f.issue,
787
+ user_id: userId,
788
+ anon_id: userId ? void 0 : processId,
789
+ session_id: f.sessionId,
790
+ url: f.url || scope.request?.url,
791
+ release: opts.release,
792
+ environment: opts.environment,
793
+ tags: { ...scope.tags, ...f.tags || {} },
794
+ flags: Object.keys(scope.flags).length ? { ...scope.flags } : void 0
795
+ };
796
+ for (const k of Object.keys(item)) if (item[k] === void 0) delete item[k];
797
+ const headers2 = { "X-Vaka-Key": opts.key, "X-Vaka-Sdk": `${SDK.name}/${SDK.version}` };
798
+ if (f.ip) headers2["X-Forwarded-For"] = f.ip;
799
+ let body;
800
+ if (files.length) {
801
+ const form = new FormData();
802
+ form.append("feedback", JSON.stringify(item));
803
+ for (const a of files) {
804
+ const data = a.data instanceof ArrayBuffer ? new Uint8Array(a.data) : a.data;
805
+ form.append("attachment", new Blob([data], { type: a.contentType || contentTypeOf(a.filename) }), a.filename);
806
+ }
807
+ body = form;
808
+ } else {
809
+ headers2["Content-Type"] = "application/json";
810
+ body = JSON.stringify(item);
811
+ }
812
+ const { context: context2 } = await import("@opentelemetry/api");
813
+ const { suppressTracing: suppressTracing2 } = await import("@opentelemetry/core");
814
+ let r;
815
+ try {
816
+ r = await context2.with(
817
+ suppressTracing2(context2.active()),
818
+ () => fetch(`${opts.endpoint}/ingest/v1/feedback`, { method: "POST", headers: headers2, body, signal: AbortSignal.timeout(3e4) })
819
+ );
820
+ } catch (e) {
821
+ throw new FeedbackError(`vaka feedback: ${String(e?.message || e)}`);
822
+ }
823
+ const text = await r.text().catch(() => "");
824
+ let json = {};
825
+ try {
826
+ json = text ? JSON.parse(text) : {};
827
+ } catch {
828
+ }
829
+ if (r.status < 200 || r.status >= 300) throw new FeedbackError(`vaka feedback: HTTP ${r.status}${json.error ? `: ${json.error}` : ""}`, r.status);
830
+ scope.addBreadcrumb({ category: "feedback", message: "user feedback sent" }, opts.maxBreadcrumbs);
831
+ return { id: json.id || String(item.id), shortId: json.short_id || "" };
832
+ }
754
833
  function track(name, props, o = {}) {
755
834
  if (!opts || !name) return;
756
835
  const scope = currentScope();
@@ -905,6 +984,7 @@ async function _reset() {
905
984
  }
906
985
  // Annotate the CommonJS export names for ESM import in node:
907
986
  0 && (module.exports = {
987
+ FeedbackError,
908
988
  SDK,
909
989
  VakaScopeSpanProcessor,
910
990
  _globalScope,
@@ -912,6 +992,7 @@ async function _reset() {
912
992
  addBreadcrumb,
913
993
  captureError,
914
994
  captureException,
995
+ captureFeedback,
915
996
  captureMessage,
916
997
  close,
917
998
  currentScope,
package/dist/index.d.ts CHANGED
@@ -110,6 +110,55 @@ export declare function captureException(error: unknown, ctx?: CaptureContext, f
110
110
  export declare const captureError: typeof captureException;
111
111
  /** Report a message as an error-like event. */
112
112
  export declare function captureMessage(message: string, level?: Level, ctx?: CaptureContext): string | undefined;
113
+ /** A file attached to feedback: PNG, JPEG or WebP, at most 5 MB. */
114
+ export interface FeedbackAttachment {
115
+ filename: string;
116
+ data: Uint8Array | ArrayBuffer;
117
+ /** Default: guessed from the file name. */
118
+ contentType?: string;
119
+ }
120
+ export interface Feedback {
121
+ /** What the user wrote (required). Longer than 5000 characters is cut. */
122
+ message: string;
123
+ /** Contact email, only when the user gave it. */
124
+ email?: string;
125
+ name?: string;
126
+ /** The error event this feedback is about (the id captureException returned). */
127
+ eventId?: string;
128
+ /** The issue this feedback is about: id or short id like `API-12`. */
129
+ issue?: string;
130
+ /** Default: the current scope's user. */
131
+ userId?: string;
132
+ /** The end user's session id, when your app has one. */
133
+ sessionId?: string;
134
+ /** Page or screen the feedback is about. Default: the current request's URL. */
135
+ url?: string;
136
+ /**
137
+ * The end user's IP address (e.g. `req.ip`). Feedback is rate limited per address, so without it
138
+ * all feedback from this server shares one limit.
139
+ */
140
+ ip?: string;
141
+ tags?: Record<string, string>;
142
+ /** Up to 3 images. */
143
+ attachments?: FeedbackAttachment[];
144
+ }
145
+ export interface FeedbackResult {
146
+ id: string;
147
+ /** e.g. `API-F12` */
148
+ shortId: string;
149
+ }
150
+ /** Thrown by captureFeedback when the server rejects the feedback or cannot be reached. */
151
+ export declare class FeedbackError extends Error {
152
+ /** HTTP status, or 0 when nothing was sent. 429 means a rate limit was hit. */
153
+ status: number;
154
+ constructor(message: string, status?: number);
155
+ }
156
+ /**
157
+ * Forward feedback your users wrote (a support form, in-app chat) to Vaka, with the current
158
+ * scope's user, release, environment, tags and flags. Sent right away, not batched.
159
+ * Rejects with a FeedbackError when the server refuses it (e.g. 429 for a rate limit).
160
+ */
161
+ export declare function captureFeedback(f: Feedback): Promise<FeedbackResult>;
113
162
  /** Record a custom server-side event (e.g. `track('order_paid', { amount: 49 })`). */
114
163
  export declare function track(name: string, props?: Props, o?: {
115
164
  userId?: string;
package/dist/index.mjs CHANGED
@@ -492,7 +492,7 @@ function makeFastifyPlugin(capture, shouldCapture2) {
492
492
  }
493
493
 
494
494
  // src/index.ts
495
- var SDK = { name: "vaka-node", version: "0.2.0" };
495
+ var SDK = { name: "vaka-node", version: "0.3.0" };
496
496
  var opts;
497
497
  var transport;
498
498
  var otelSdk;
@@ -687,6 +687,83 @@ function captureMessage(message, level = "info", ctx = {}) {
687
687
  const item = { type: "error", ...baseItem(currentScope(), ctx), level, message, fingerprint: ctx.fingerprint };
688
688
  return enqueue(item, level === "error" || level === "fatal") ? item.event_id : void 0;
689
689
  }
690
+ var FeedbackError = class extends Error {
691
+ /** HTTP status, or 0 when nothing was sent. 429 means a rate limit was hit. */
692
+ status;
693
+ constructor(message, status = 0) {
694
+ super(message);
695
+ this.name = "FeedbackError";
696
+ this.status = status;
697
+ }
698
+ };
699
+ function contentTypeOf(filename) {
700
+ const ext = filename.toLowerCase().split(".").pop();
701
+ return ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : "image/jpeg";
702
+ }
703
+ async function captureFeedback(f) {
704
+ if (!opts || !transport) throw new FeedbackError("vaka is not initialized");
705
+ const message = String(f?.message ?? "").trim();
706
+ if (!message) throw new FeedbackError("feedback needs a message");
707
+ const files = (f.attachments || []).filter(Boolean);
708
+ if (files.length > 3) throw new FeedbackError("at most 3 attachments per feedback");
709
+ const scope = currentScope();
710
+ const userId = f.userId || scope.user?.id;
711
+ const item = {
712
+ id: randomUUID(),
713
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
714
+ platform: "node",
715
+ source: "api",
716
+ message: message.slice(0, 5e3),
717
+ email: f.email?.trim() || void 0,
718
+ name: f.name?.trim() || void 0,
719
+ event_id: f.eventId,
720
+ issue: f.issue,
721
+ user_id: userId,
722
+ anon_id: userId ? void 0 : processId,
723
+ session_id: f.sessionId,
724
+ url: f.url || scope.request?.url,
725
+ release: opts.release,
726
+ environment: opts.environment,
727
+ tags: { ...scope.tags, ...f.tags || {} },
728
+ flags: Object.keys(scope.flags).length ? { ...scope.flags } : void 0
729
+ };
730
+ for (const k of Object.keys(item)) if (item[k] === void 0) delete item[k];
731
+ const headers2 = { "X-Vaka-Key": opts.key, "X-Vaka-Sdk": `${SDK.name}/${SDK.version}` };
732
+ if (f.ip) headers2["X-Forwarded-For"] = f.ip;
733
+ let body;
734
+ if (files.length) {
735
+ const form = new FormData();
736
+ form.append("feedback", JSON.stringify(item));
737
+ for (const a of files) {
738
+ const data = a.data instanceof ArrayBuffer ? new Uint8Array(a.data) : a.data;
739
+ form.append("attachment", new Blob([data], { type: a.contentType || contentTypeOf(a.filename) }), a.filename);
740
+ }
741
+ body = form;
742
+ } else {
743
+ headers2["Content-Type"] = "application/json";
744
+ body = JSON.stringify(item);
745
+ }
746
+ const { context: context2 } = await import("@opentelemetry/api");
747
+ const { suppressTracing: suppressTracing2 } = await import("@opentelemetry/core");
748
+ let r;
749
+ try {
750
+ r = await context2.with(
751
+ suppressTracing2(context2.active()),
752
+ () => fetch(`${opts.endpoint}/ingest/v1/feedback`, { method: "POST", headers: headers2, body, signal: AbortSignal.timeout(3e4) })
753
+ );
754
+ } catch (e) {
755
+ throw new FeedbackError(`vaka feedback: ${String(e?.message || e)}`);
756
+ }
757
+ const text = await r.text().catch(() => "");
758
+ let json = {};
759
+ try {
760
+ json = text ? JSON.parse(text) : {};
761
+ } catch {
762
+ }
763
+ if (r.status < 200 || r.status >= 300) throw new FeedbackError(`vaka feedback: HTTP ${r.status}${json.error ? `: ${json.error}` : ""}`, r.status);
764
+ scope.addBreadcrumb({ category: "feedback", message: "user feedback sent" }, opts.maxBreadcrumbs);
765
+ return { id: json.id || String(item.id), shortId: json.short_id || "" };
766
+ }
690
767
  function track(name, props, o = {}) {
691
768
  if (!opts || !name) return;
692
769
  const scope = currentScope();
@@ -840,6 +917,7 @@ async function _reset() {
840
917
  _resetScopes();
841
918
  }
842
919
  export {
920
+ FeedbackError,
843
921
  SDK,
844
922
  VakaScopeSpanProcessor,
845
923
  globalScope as _globalScope,
@@ -847,6 +925,7 @@ export {
847
925
  addBreadcrumb,
848
926
  captureError,
849
927
  captureException,
928
+ captureFeedback,
850
929
  captureMessage,
851
930
  close,
852
931
  currentScope,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hellsoft/vaka-node",
3
3
  "publishConfig": {"access": "public"},
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "Vaka Node.js SDK: crash reporting, OpenTelemetry tracing and logs, feature flags",
6
6
  "license": "MIT OR Apache-2.0",
7
7
  "repository": {