@kelet-ai/feedback-ui 0.7.1 → 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.
package/README.md CHANGED
@@ -261,7 +261,7 @@ For implicit feedback, understanding how state changes are processed:
261
261
  // User types: "Hello" → "Hello World" → "Hello World!"
262
262
  // Only sends ONE feedback after user stops typing
263
263
  const [text, setText] = useFeedbackState('', 'editor', {
264
- debounceMs: 1500, // Wait 1.5s after last change
264
+ debounceMs: 3000, // Wait 3s after last change
265
265
  });
266
266
  ```
267
267
 
@@ -321,18 +321,18 @@ const [data, setData] = useFeedbackState(initial, 'tracker', {
321
321
 
322
322
  ## 🔗 OpenTelemetry Integration
323
323
 
324
- Automatically extract trace IDs to correlate feedback with distributed traces:
324
+ Prefer W3C traceparent to correlate feedback with distributed traces:
325
325
 
326
326
  ```tsx
327
- import { VoteFeedback, getOtelTraceId } from '@kelet-ai/feedback-ui';
327
+ import { VoteFeedback, getTraceParent } from '@kelet-ai/feedback-ui';
328
328
 
329
- <VoteFeedback.Root tx_id={getOtelTraceId} onFeedback={handleFeedback}>
329
+ <VoteFeedback.Root tx_id={getTraceParent} onFeedback={handleFeedback}>
330
330
  <VoteFeedback.UpvoteButton>👍</VoteFeedback.UpvoteButton>
331
331
  <VoteFeedback.DownvoteButton>👎</VoteFeedback.DownvoteButton>
332
332
  </VoteFeedback.Root>;
333
333
  ```
334
334
 
335
- Requires `@opentelemetry/api` and active Span to collect the trace_id from.
335
+ Requires `@opentelemetry/api` for active context. If missing at runtime, an error is thrown.
336
336
 
337
337
  ---
338
338
 
@@ -535,7 +535,7 @@ interface FeedbackData {
535
535
 
536
536
  | Option | Type | Default | Description |
537
537
  | ---------------------- | ------------------------------------ | --------------------- | -------------------------------------- |
538
- | `debounceMs` | `number` | `1500` | Debounce time in milliseconds |
538
+ | `debounceMs` | `number` | `3000` | Debounce time in milliseconds |
539
539
  | `diffType` | `'git' \| 'object' \| 'json'` | `'git'` | Diff output format |
540
540
  | `compareWith` | `(a: T, b: T) => boolean` | `undefined` | Custom equality function |
541
541
  | `metadata` | `Record<string, any>` | `{}` | Additional metadata |
@@ -1865,7 +1865,7 @@ function splitLines(text) {
1865
1865
  }
1866
1866
  return result;
1867
1867
  }
1868
- function formatDiff(oldValue, newValue, diffType = "git", context = 3) {
1868
+ function formatDiff(oldValue, newValue, diffType = "git", context = 1) {
1869
1869
  switch (diffType) {
1870
1870
  case "git":
1871
1871
  return formatGitDiff(oldValue, newValue, context);
@@ -1877,7 +1877,7 @@ function formatDiff(oldValue, newValue, diffType = "git", context = 3) {
1877
1877
  return formatGitDiff(oldValue, newValue, context);
1878
1878
  }
1879
1879
  }
1880
- function formatGitDiff(oldValue, newValue, context = 3) {
1880
+ function formatGitDiff(oldValue, newValue, context = 1) {
1881
1881
  const oldStr = stringify(oldValue);
1882
1882
  const newStr = stringify(newValue);
1883
1883
  const patch = createTwoFilesPatch(
@@ -1893,7 +1893,20 @@ function formatGitDiff(oldValue, newValue, context = 3) {
1893
1893
  "",
1894
1894
  { context }
1895
1895
  );
1896
- return patch.split("\n").slice(2).join("\n");
1896
+ const lines = patch.split("\n");
1897
+ const filtered = lines.filter((line) => {
1898
+ if (!line) return false;
1899
+ if (line === "\") return false;
1900
+ if (line.startsWith("@@")) return false;
1901
+ if (line.startsWith("---") || line.startsWith("+++")) return false;
1902
+ if (line.startsWith(" ")) return false;
1903
+ if (line.startsWith("+") || line.startsWith("-")) return true;
1904
+ return false;
1905
+ });
1906
+ while (filtered.length && filtered[0].trim() === "") filtered.shift();
1907
+ while (filtered.length && filtered[filtered.length - 1].trim() === "")
1908
+ filtered.pop();
1909
+ return filtered.join("\n");
1897
1910
  }
1898
1911
  function formatObjectDiff(oldValue, newValue) {
1899
1912
  const differences = deepDiffExports.diff(oldValue, newValue) || [];
@@ -2006,19 +2019,22 @@ function stringify(value) {
2006
2019
  function useStateChangeTracking(currentState, tx_id, options) {
2007
2020
  const defaultFeedbackHandler = useDefaultFeedbackHandler();
2008
2021
  const feedbackHandler = options?.onFeedback || defaultFeedbackHandler;
2009
- const debounceMs = options?.debounceMs ?? 1500;
2022
+ const debounceMs = options?.debounceMs ?? 3e3;
2010
2023
  const diffType = options?.diffType ?? "git";
2011
2024
  const compareWith = options?.compareWith;
2012
2025
  const defaultTriggerName = options?.default_trigger_name ?? "auto_state_change";
2013
2026
  const ignoreInitialNullish = options?.ignoreInitialNullish ?? true;
2014
2027
  const prevStateRef = useRef(currentState);
2015
- const changeStartStateRef = useRef(currentState);
2016
2028
  const isFirstRenderRef = useRef(true);
2017
2029
  const initialStateRef = useRef(currentState);
2030
+ const initialWasNullishRef = useRef(currentState == null);
2018
2031
  const hasHadNonNullishStateRef = useRef(
2019
2032
  currentState != null
2020
2033
  // != null catches both null and undefined
2021
2034
  );
2035
+ const hasEligibleBaselineRef = useRef(
2036
+ !(ignoreInitialNullish && currentState == null)
2037
+ );
2022
2038
  const timeoutRef = useRef(null);
2023
2039
  const currentTriggerNameRef = useRef(void 0);
2024
2040
  const sendFeedback = useCallback(
@@ -2053,7 +2069,7 @@ function useStateChangeTracking(currentState, tx_id, options) {
2053
2069
  const newTriggerName = trigger_name || defaultTriggerName;
2054
2070
  if (timeoutRef.current && currentTriggerNameRef.current && currentTriggerNameRef.current !== newTriggerName) {
2055
2071
  clearTimeout(timeoutRef.current);
2056
- const startState = changeStartStateRef.current;
2072
+ const startState = initialStateRef.current;
2057
2073
  const currentStateBeforeChange = currentState;
2058
2074
  sendFeedback(
2059
2075
  startState,
@@ -2070,39 +2086,41 @@ function useStateChangeTracking(currentState, tx_id, options) {
2070
2086
  if (isFirstRenderRef.current) {
2071
2087
  isFirstRenderRef.current = false;
2072
2088
  prevStateRef.current = currentState;
2073
- changeStartStateRef.current = currentState;
2089
+ if (!ignoreInitialNullish || currentState != null) {
2090
+ initialStateRef.current = currentState;
2091
+ hasEligibleBaselineRef.current = true;
2092
+ }
2074
2093
  return;
2075
2094
  }
2076
2095
  const prevState = prevStateRef.current;
2077
2096
  const isEqual = compareWith ? compareWith(prevState, currentState) : JSON.stringify(prevState) === JSON.stringify(currentState);
2078
2097
  if (!isEqual) {
2079
- const shouldIgnoreChange = ignoreInitialNullish && initialStateRef.current == null && // Initial state was nullish
2098
+ const shouldIgnoreChange = ignoreInitialNullish && initialWasNullishRef.current && // True initial state was nullish
2080
2099
  !hasHadNonNullishStateRef.current && // We haven't had non-nullish state before
2081
2100
  currentState != null;
2082
2101
  if (currentState != null) {
2083
2102
  hasHadNonNullishStateRef.current = true;
2084
2103
  }
2085
2104
  if (shouldIgnoreChange) {
2105
+ initialStateRef.current = currentState;
2106
+ hasEligibleBaselineRef.current = true;
2086
2107
  prevStateRef.current = currentState;
2087
- changeStartStateRef.current = currentState;
2088
2108
  return;
2089
2109
  }
2090
- if (!timeoutRef.current) {
2091
- changeStartStateRef.current = prevState;
2092
- }
2093
2110
  if (timeoutRef.current) {
2094
2111
  clearTimeout(timeoutRef.current);
2095
2112
  }
2096
2113
  prevStateRef.current = currentState;
2097
2114
  timeoutRef.current = setTimeout(() => {
2098
- const startState = changeStartStateRef.current;
2115
+ const startState = initialStateRef.current;
2099
2116
  const finalState = currentState;
2100
- sendFeedback(
2101
- startState,
2102
- finalState,
2103
- currentTriggerNameRef.current || defaultTriggerName
2104
- );
2105
- changeStartStateRef.current = finalState;
2117
+ if (hasEligibleBaselineRef.current) {
2118
+ sendFeedback(
2119
+ startState,
2120
+ finalState,
2121
+ currentTriggerNameRef.current || defaultTriggerName
2122
+ );
2123
+ }
2106
2124
  timeoutRef.current = null;
2107
2125
  }, debounceMs);
2108
2126
  }
@@ -2142,43 +2160,42 @@ function useFeedbackState(initialState, tx_id, options) {
2142
2160
  );
2143
2161
  return [state, setState];
2144
2162
  }
2145
- let _loadOtelApi = () => {
2146
- try {
2147
- return require("@opentelemetry/api");
2148
- } catch {
2163
+ const _loadOtelApi = () => {
2164
+ const g = globalThis;
2165
+ const store = g[Symbol.for("opentelemetry.js.api.1")];
2166
+ const api = store?.api ?? store;
2167
+ if (!api?.context || !api?.propagation) {
2149
2168
  throw new Error(
2150
- "OpenTelemetry is not available. Install @opentelemetry/api to use trace ID extraction."
2169
+ "@opentelemetry/api not found. Install it and configure a tracer provider."
2151
2170
  );
2152
2171
  }
2172
+ return api;
2153
2173
  };
2154
- function getOtelTraceId() {
2155
- const { trace, context } = _loadOtelApi();
2174
+ function getTraceParent(_ = null) {
2156
2175
  try {
2157
- const spanContext = trace.getSpanContext(context.active());
2158
- if (spanContext?.traceId) {
2159
- return spanContext.traceId;
2160
- }
2161
- const activeSpan = trace.getSpan(context.active());
2162
- if (activeSpan) {
2163
- const spanCtx = activeSpan.spanContext();
2164
- if (spanCtx.traceId) {
2165
- return spanCtx.traceId;
2176
+ const { context, propagation } = _loadOtelApi();
2177
+ const carrier = {};
2178
+ propagation.inject(context.active(), carrier, {
2179
+ set: (c, k, v) => {
2180
+ c[k] = v;
2166
2181
  }
2182
+ });
2183
+ const traceparent = carrier["traceparent"];
2184
+ if (!traceparent) {
2185
+ throw new Error("traceparent header not available from active context");
2167
2186
  }
2187
+ return traceparent;
2168
2188
  } catch (error) {
2169
2189
  throw new Error(
2170
- `Failed to extract OpenTelemetry trace ID: ${error instanceof Error ? error.message : "Unknown error"}`
2190
+ `Failed to extract traceparent: ${error instanceof Error ? error.message : "Unknown error"}`
2171
2191
  );
2172
2192
  }
2173
- throw new Error(
2174
- "OpenTelemetry trace ID not available. Ensure XHR/Fetch instrumentation is active and a request is in progress."
2175
- );
2176
2193
  }
2177
2194
  export {
2178
2195
  KeletContext,
2179
2196
  KeletProvider,
2180
2197
  VoteFeedback,
2181
- getOtelTraceId,
2198
+ getTraceParent,
2182
2199
  useDefaultFeedbackHandler,
2183
2200
  useFeedbackState,
2184
2201
  useKelet