Merge pull request #267 from buster-so/staging

raindrop
This commit is contained in:
dal 2025-05-05 15:32:11 -07:00 committed by GitHub
commit eb24d0fe0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 406 additions and 75 deletions

View File

@ -12,6 +12,7 @@ members = [
"libs/dataset_security", "libs/dataset_security",
"libs/email", "libs/email",
"libs/stored_values", "libs/stored_values",
"libs/raindrop",
] ]
resolver = "2" resolver = "2"

View File

@ -34,6 +34,7 @@ sqlx = { workspace = true }
stored_values = { path = "../stored_values" } stored_values = { path = "../stored_values" }
tokio-retry = { workspace = true } tokio-retry = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
raindrop = { path = "../raindrop" }
sql_analyzer = { path = "../sql_analyzer" } sql_analyzer = { path = "../sql_analyzer" }
# Development dependencies # Development dependencies

View File

@ -11,9 +11,13 @@ use std::time::{Duration, Instant};
use std::{collections::HashMap, env, sync::Arc}; use std::{collections::HashMap, env, sync::Arc};
use tokio::sync::{broadcast, mpsc, RwLock}; use tokio::sync::{broadcast, mpsc, RwLock};
use tokio_retry::{strategy::ExponentialBackoff, Retry}; use tokio_retry::{strategy::ExponentialBackoff, Retry};
use tracing::{error, warn}; use tracing::{debug, error, info, instrument, warn};
use uuid::Uuid; use uuid::Uuid;
// Raindrop imports
use raindrop::types::{AiData as RaindropAiData, Event as RaindropEvent};
use raindrop::RaindropClient;
// Type definition for tool registry to simplify complex type // Type definition for tool registry to simplify complex type
// No longer needed, defined below // No longer needed, defined below
use crate::models::AgentThread; use crate::models::AgentThread;
@ -561,6 +565,9 @@ impl Agent {
trace_builder: Option<TraceBuilder>, trace_builder: Option<TraceBuilder>,
parent_span: Option<braintrust::Span>, parent_span: Option<braintrust::Span>,
) -> Result<()> { ) -> Result<()> {
// Attempt to initialize Raindrop client (non-blocking)
let raindrop_client = RaindropClient::new().ok();
// Set the initial thread // Set the initial thread
{ {
let mut current = agent.current_thread.write().await; let mut current = agent.current_thread.write().await;
@ -721,6 +728,35 @@ impl Agent {
..Default::default() ..Default::default()
}; };
// --- Track Request with Raindrop ---
if let Some(client) = raindrop_client.clone() {
let request_clone = request.clone(); // Clone request for tracking
let user_id = agent.user_id.clone();
let session_id = agent.session_id.to_string();
let current_history = agent.get_conversation_history().await.unwrap_or_default();
tokio::spawn(async move {
let event = RaindropEvent {
user_id: user_id.to_string(),
event: "llm_request".to_string(),
properties: Some(HashMap::from([(
"conversation_history".to_string(),
serde_json::to_value(&current_history).unwrap_or(Value::Null),
)])),
attachments: None,
ai_data: Some(RaindropAiData {
model: request_clone.model.clone(),
input: serde_json::to_string(&request_clone.messages).unwrap_or_default(),
output: "".to_string(), // Output is not known yet
convo_id: Some(session_id.clone()),
}),
event_id: None, // Raindrop assigns this
timestamp: Some(chrono::Utc::now()),
};
if let Err(e) = client.track_events(vec![event]).await {}
});
}
// --- End Track Request ---
// --- Retry Logic for Initial Stream Request --- // --- Retry Logic for Initial Stream Request ---
let retry_strategy = ExponentialBackoff::from_millis(100).take(3); // Retry 3 times, ~100ms, ~200ms, ~400ms let retry_strategy = ExponentialBackoff::from_millis(100).take(3); // Retry 3 times, ~100ms, ~200ms, ~400ms
@ -980,6 +1016,37 @@ impl Agent {
// Update thread with assistant message // Update thread with assistant message
agent.update_current_thread(final_message.clone()).await?; agent.update_current_thread(final_message.clone()).await?;
// --- Track Response with Raindrop ---
if let Some(client) = raindrop_client {
let request_clone = request.clone(); // Clone again for response tracking
let final_message_clone = final_message.clone();
let user_id = agent.user_id.clone();
let session_id = agent.session_id.to_string();
// Get history *after* adding the final message
let current_history = agent.get_conversation_history().await.unwrap_or_default();
tokio::spawn(async move {
let event = RaindropEvent {
user_id: user_id.to_string(),
event: "llm_response".to_string(),
properties: Some(HashMap::from([(
"conversation_history".to_string(),
serde_json::to_value(&current_history).unwrap_or(Value::Null),
)])),
attachments: None,
ai_data: Some(RaindropAiData {
model: request_clone.model.clone(),
input: serde_json::to_string(&request_clone.messages).unwrap_or_default(),
output: serde_json::to_string(&final_message_clone).unwrap_or_default(),
convo_id: Some(session_id.clone()),
}),
event_id: None, // Raindrop assigns this
timestamp: Some(chrono::Utc::now()),
};
if let Err(e) = client.track_events(vec![event]).await {}
});
}
// --- End Track Response ---
// Get the updated thread state AFTER adding the final assistant message // Get the updated thread state AFTER adding the final assistant message
// This will be used for the potential recursive call later. // This will be used for the potential recursive call later.
let mut updated_thread_for_recursion = agent let mut updated_thread_for_recursion = agent
@ -1825,4 +1892,3 @@ mod tests {
assert_eq!(agent.get_state_bool("bool_key").await, None); assert_eq!(agent.get_state_bool("bool_key").await, None);
} }
} }

View File

@ -0,0 +1,22 @@
[package]
name = "raindrop"
version = "0.1.0"
edition = "2021"
[dependencies]
# Workspace dependencies
anyhow = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tracing = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
thiserror = { workspace = true }
reqwest = { workspace = true, features = ["json", "rustls-tls"] }
# Non-workspace dependencies (if any, prefer workspace)
dotenvy = "0.15"
[dev-dependencies]
tokio-test = { workspace = true }
mockito = { workspace = true }

View File

@ -0,0 +1,27 @@
use thiserror::Error;
use reqwest::header::InvalidHeaderValue;
/// Custom error types for the Raindrop SDK.
#[derive(Error, Debug)]
pub enum RaindropError {
#[error("Missing Raindrop API Write Key. Set the RAINDROP_WRITE_KEY environment variable.")]
MissingApiKey,
#[error("Invalid header value provided: {0}")]
InvalidHeaderValue(#[from] InvalidHeaderValue),
#[error("Failed to build HTTP client: {0}")]
HttpClientBuildError(#[from] reqwest::Error),
#[error("HTTP request failed: {0}")]
RequestError(reqwest::Error),
#[error("Raindrop API error: {status} - {body}")]
ApiError {
status: reqwest::StatusCode,
body: String,
},
#[error("Failed to serialize request body: {0}")]
SerializationError(#[from] serde_json::Error),
}

View File

@ -0,0 +1,114 @@
#![doc = "A Rust SDK for interacting with the Raindrop.ai API."]
pub mod errors;
pub mod types;
use anyhow::Context;
use reqwest::{Client, header};
use std::env;
use tracing::{debug, error, instrument};
use errors::RaindropError;
use types::{Event, Signal};
const DEFAULT_BASE_URL: &str = "https://api.raindrop.ai/v1";
/// Client for interacting with the Raindrop API.
#[derive(Debug, Clone)]
pub struct RaindropClient {
client: Client,
base_url: String,
write_key: String,
}
impl RaindropClient {
/// Creates a new RaindropClient.
/// Reads the write key from the `RAINDROP_WRITE_KEY` environment variable.
/// Uses the default Raindrop API base URL.
pub fn new() -> Result<Self, RaindropError> {
let write_key = env::var("RAINDROP_WRITE_KEY")
.map_err(|_| RaindropError::MissingApiKey)?;
let base_url = DEFAULT_BASE_URL.to_string();
Self::build_client(write_key, base_url)
}
/// Creates a new RaindropClient with a specific write key and base URL.
/// Useful for testing or custom deployments.
pub fn with_key_and_url(write_key: String, base_url: &str) -> Result<Self, RaindropError> {
Self::build_client(write_key, base_url.to_string())
}
/// Builds the underlying reqwest client.
fn build_client(write_key: String, base_url: String) -> Result<Self, RaindropError> {
let mut headers = header::HeaderMap::new();
headers.insert(
header::AUTHORIZATION,
header::HeaderValue::from_str(&format!("Bearer {}", write_key))
.map_err(RaindropError::InvalidHeaderValue)?,
);
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
);
let client = Client::builder()
.default_headers(headers)
.build()
.map_err(RaindropError::HttpClientBuildError)?;
Ok(Self {
client,
base_url,
write_key,
})
}
/// Tracks a batch of events.
#[instrument(skip(self, events), fields(count = events.len()))]
pub async fn track_events(&self, events: Vec<Event>) -> Result<(), RaindropError> {
if events.is_empty() {
debug!("No events to track, skipping API call.");
return Ok(());
}
let url = format!("{}/events/track", self.base_url);
self.post_data(&url, &events).await
}
/// Tracks a batch of signals.
#[instrument(skip(self, signals), fields(count = signals.len()))]
pub async fn track_signals(&self, signals: Vec<Signal>) -> Result<(), RaindropError> {
if signals.is_empty() {
debug!("No signals to track, skipping API call.");
return Ok(());
}
let url = format!("{}/signals/track", self.base_url);
self.post_data(&url, &signals).await
}
/// Helper function to POST JSON data to a specified URL.
async fn post_data<T: serde::Serialize>(
&self,
url: &str,
data: &T,
) -> Result<(), RaindropError> {
debug!(url = url, "Sending POST request to Raindrop");
let response = self
.client
.post(url)
.json(data)
.send()
.await
.map_err(RaindropError::RequestError)?;
let status = response.status();
if status.is_success() {
debug!(url = url, status = %status, "Raindrop API call successful");
Ok(())
} else {
let body = response.text().await.unwrap_or_else(|_| "Failed to read error body".to_string());
error!(url = url, status = %status, body = body, "Raindrop API call failed");
Err(RaindropError::ApiError { status, body })
}
}
}

View File

@ -0,0 +1,67 @@
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// Represents a single event to be tracked.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Event {
pub user_id: String,
pub event: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<HashMap<String, Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<Attachment>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_data: Option<AiData>,
// Optional fields provided by Raindrop API
#[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>, // Returned by Raindrop, optional on send
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<DateTime<Utc>>,
}
/// Represents an attachment associated with an event.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Attachment {
#[serde(rename = "type")] // Use `type` keyword in JSON
pub attachment_type: String, // e.g., "image", "text", "json"
pub value: String, // URL or content
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>, // e.g., "input", "output"
}
/// Represents AI-specific data for an event.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AiData {
pub model: String,
pub input: String,
pub output: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub convo_id: Option<String>,
}
/// Represents a single signal to be tracked.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Signal {
pub event_id: String, // The ID of the event this signal relates to
pub signal_name: String, // e.g., "thumbs_down", "corrected_answer"
pub signal_type: String, // e.g., "feedback", "correction"
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<HashMap<String, Value>>,
// Optional fields
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<DateTime<Utc>>,
}

View File

@ -195,9 +195,6 @@ export const BusterChartJSComponent = React.memo(
if (selectedChartType === 'combo') return [ChartHoverBarPlugin, ChartTotalizerPlugin]; if (selectedChartType === 'combo') return [ChartHoverBarPlugin, ChartTotalizerPlugin];
return []; return [];
}, [selectedChartType]); }, [selectedChartType]);
console.log('datasetOptions', datasetOptions);
console.log('data', data);
console.log('options', options);
return ( return (
<ChartMountedWrapper> <ChartMountedWrapper>

View File

@ -1,6 +1,6 @@
import { formatBarAndLineDataLabel } from './formatBarAndLineDataLabel'; import { formatBarAndLineDataLabel } from './formatBarAndLineDataLabel';
import { ColumnLabelFormat } from '@/api/asset_interfaces/metric'; import { ColumnLabelFormat } from '@/api/asset_interfaces/metric';
import { Context } from 'chartjs-plugin-datalabels'; import type { Context } from 'chartjs-plugin-datalabels';
describe('formatBarAndLineDataLabel', () => { describe('formatBarAndLineDataLabel', () => {
it('formats a single value without percentage', () => { it('formats a single value without percentage', () => {
@ -33,4 +33,89 @@ describe('formatBarAndLineDataLabel', () => {
expect(result).toBe('1,234.56'); expect(result).toBe('1,234.56');
}); });
// Mock chart context
const createMockContext = (datasets: any[]): Partial<Context> => ({
chart: {
data: {
datasets
},
$totalizer: {
stackTotals: [100],
seriesTotals: [50]
}
} as any,
dataIndex: 0,
datasetIndex: 0
});
describe('useStackTotal logic', () => {
const baseDataset = { hidden: false, isTrendline: false };
test('should use stack total when there are multiple visible datasets', () => {
const mockContext = createMockContext([baseDataset, { ...baseDataset }]) as Context;
const result = formatBarAndLineDataLabel(25, mockContext, 'data-label', {
style: 'number',
columnType: 'number'
});
// 25 out of stack total (100) = 25%
expect(result).toBe('25%');
});
test('should use stack total when percentageMode is stacked', () => {
const mockContext = createMockContext([baseDataset]) as Context;
const result = formatBarAndLineDataLabel(25, mockContext, 'stacked', {
style: 'number',
columnType: 'number'
});
// 25 out of stack total (100) = 25%
expect(result).toBe('25%');
});
test('should use series total for single dataset and non-stacked percentage mode', () => {
const mockContext = createMockContext([baseDataset]) as Context;
const result = formatBarAndLineDataLabel(25, mockContext, 'data-label', {
style: 'number',
columnType: 'number'
});
// 25 out of series total (50) = 50%
expect(result).toBe('50%');
});
test('should ignore hidden datasets when counting multiple datasets', () => {
const mockContext = createMockContext([
baseDataset,
{ ...baseDataset, hidden: true }
]) as Context;
const result = formatBarAndLineDataLabel(25, mockContext, 'data-label', {
style: 'number',
columnType: 'number'
});
// 25 out of series total (50) = 50% (since second dataset is hidden)
expect(result).toBe('50%');
});
test('should ignore trendline datasets when counting multiple datasets', () => {
const mockContext = createMockContext([
baseDataset,
{ ...baseDataset, isTrendline: true }
]) as Context;
const result = formatBarAndLineDataLabel(25, mockContext, 'data-label', {
style: 'number',
columnType: 'number'
});
// 25 out of series total (50) = 50% (since second dataset is a trendline)
expect(result).toBe('50%');
});
});
}); });

View File

@ -17,7 +17,7 @@ export const formatBarAndLineDataLabel = (
); );
const hasMultipleDatasets = shownDatasets.length > 1; const hasMultipleDatasets = shownDatasets.length > 1;
const useStackTotal = !hasMultipleDatasets || percentageMode === 'stacked'; const useStackTotal = hasMultipleDatasets || percentageMode === 'stacked';
const total: number = useStackTotal const total: number = useStackTotal
? context.chart.$totalizer.stackTotals[context.dataIndex] ? context.chart.$totalizer.stackTotals[context.dataIndex]

View File

@ -332,13 +332,12 @@ const getFormattedValueAndSetBarDataLabels = (
} }
) => { ) => {
const rawValue = context.dataset.data[context.dataIndex] as number; const rawValue = context.dataset.data[context.dataIndex] as number;
const percentageModesMatch = context.chart.$barDataLabelsPercentageMode === percentageMode; const formattedValue = formatBarAndLineDataLabel(
const currentValue = percentageModesMatch rawValue,
? context.chart.$barDataLabels?.[context.datasetIndex]?.[context.dataIndex] || '' context,
: ''; percentageMode,
columnLabelFormat
const formattedValue = );
currentValue || formatBarAndLineDataLabel(rawValue, context, percentageMode, columnLabelFormat);
// Store only the formatted value, rotation is handled globally // Store only the formatted value, rotation is handled globally
setBarDataLabelsManager(context, formattedValue, percentageMode); setBarDataLabelsManager(context, formattedValue, percentageMode);

View File

@ -52,14 +52,6 @@ export const useAutoChangeLayout = ({
const hasReasoning = !!lastReasoningMessageId; const hasReasoning = !!lastReasoningMessageId;
useEffect(() => { useEffect(() => {
console.log(
'REASONING: useEffect',
isCompletedStream,
hasReasoning,
chatId,
lastMessageId,
isFinishedReasoning
);
//this will trigger when the chat is streaming and is has not completed yet (new chat) //this will trigger when the chat is streaming and is has not completed yet (new chat)
if ( if (
!isCompletedStream && !isCompletedStream &&
@ -70,14 +62,11 @@ export const useAutoChangeLayout = ({
) { ) {
previousLastMessageId.current = lastMessageId; previousLastMessageId.current = lastMessageId;
console.log('REASONING: FLIP TO REASONING!', lastMessageId);
onSetSelectedFile({ id: lastMessageId, type: 'reasoning', versionNumber: undefined }); onSetSelectedFile({ id: lastMessageId, type: 'reasoning', versionNumber: undefined });
} }
//this will when the chat is completed and it WAS streaming //this will when the chat is completed and it WAS streaming
else if (isCompletedStream && previousIsCompletedStream === false) { else if (isCompletedStream && previousIsCompletedStream === false) {
console.log('REASONING: SELECT STREAMING FILE');
const chatMessage = getChatMessageMemoized(lastMessageId); const chatMessage = getChatMessageMemoized(lastMessageId);
const lastFileId = findLast(chatMessage?.response_message_ids, (id) => { const lastFileId = findLast(chatMessage?.response_message_ids, (id) => {
const responseMessage = chatMessage?.response_messages[id]; const responseMessage = chatMessage?.response_messages[id];
@ -97,7 +86,6 @@ export const useAutoChangeLayout = ({
}); });
if (link) { if (link) {
console.log('auto change layout', link);
onChangePage(link); onChangePage(link);
} }
return; return;
@ -105,7 +93,6 @@ export const useAutoChangeLayout = ({
} }
//this will trigger on a page refresh and the chat is completed //this will trigger on a page refresh and the chat is completed
else if (isCompletedStream && chatId) { else if (isCompletedStream && chatId) {
console.log('REASONING: SELECT INITIAL CHAT FILE - PAGE LOAD');
const isChatOnlyMode = !metricId && !dashboardId && !messageId; const isChatOnlyMode = !metricId && !dashboardId && !messageId;
if (isChatOnlyMode) { if (isChatOnlyMode) {
return; return;
@ -122,7 +109,6 @@ export const useAutoChangeLayout = ({
}); });
if (href) { if (href) {
console.log('auto change layout2', href);
onChangePage(href); onChangePage(href);
} }
} }

View File

@ -17,7 +17,6 @@ export const useChatLayoutContext = ({ appSplitterRef }: UseLayoutConfigProps) =
const chatParams = useGetChatParams(); const chatParams = useGetChatParams();
const animateOpenSplitter = useMemoizedFn((side: 'left' | 'right' | 'both') => { const animateOpenSplitter = useMemoizedFn((side: 'left' | 'right' | 'both') => {
console.log('animateOpenSplitter', !!appSplitterRef.current, { side });
if (appSplitterRef.current) { if (appSplitterRef.current) {
const { animateWidth, sizes } = appSplitterRef.current; const { animateWidth, sizes } = appSplitterRef.current;
const leftSize = sizes[0] ?? 0; const leftSize = sizes[0] ?? 0;

View File

@ -88,11 +88,6 @@ export const useLayoutConfig = ({
fileId?: string | undefined; fileId?: string | undefined;
secondaryView?: FileViewSecondary; secondaryView?: FileViewSecondary;
}) => { }) => {
console.log('onSetFileView', {
fileView,
fileId: fileIdProp,
secondaryView
});
const fileId = fileIdProp ?? selectedFileId; const fileId = fileIdProp ?? selectedFileId;
if (!fileId) { if (!fileId) {
onCollapseFileClick(); onCollapseFileClick();
@ -146,11 +141,7 @@ export const useLayoutConfig = ({
const closeSecondaryView = useMemoizedFn(async () => { const closeSecondaryView = useMemoizedFn(async () => {
if (!selectedFileId || !selectedFileViewConfig || !selectedFileView) return; if (!selectedFileId || !selectedFileViewConfig || !selectedFileView) return;
console.log('closeSecondaryView', {
selectedFileId,
selectedFileViewConfig,
selectedFileView
});
setFileViews((prev) => { setFileViews((prev) => {
return create(prev, (draft) => { return create(prev, (draft) => {
if (!draft[selectedFileId]?.fileViewConfig?.[selectedFileView]) return; if (!draft[selectedFileId]?.fileViewConfig?.[selectedFileView]) return;
@ -162,9 +153,7 @@ export const useLayoutConfig = ({
}); });
const onCollapseFileClick = useMemoizedFn(async () => { const onCollapseFileClick = useMemoizedFn(async () => {
console.log('onCollapseFileClick');
const isSecondaryViewOpen = !!selectedFileViewSecondary; const isSecondaryViewOpen = !!selectedFileViewSecondary;
console.log('isSecondaryViewOpen', chatId, isSecondaryViewOpen);
if (isSecondaryViewOpen) { if (isSecondaryViewOpen) {
closeSecondaryView(); closeSecondaryView();
@ -190,15 +179,6 @@ export const useLayoutConfig = ({
//we need to use for when the user clicks the back or forward in the browser //we need to use for when the user clicks the back or forward in the browser
useUpdateEffect(() => { useUpdateEffect(() => {
console.log('useUpdateEffect', {
metricId,
secondaryView,
chatId,
dashboardId,
messageId,
currentRoute
});
const newInitialFileViews = initializeFileViews({ const newInitialFileViews = initializeFileViews({
secondaryView, secondaryView,
metricId, metricId,
@ -218,17 +198,8 @@ export const useLayoutConfig = ({
currentRoute currentRoute
}); });
console.log('isFileViewsChanged', isFileViewsChanged);
if (!isFileViewsChanged) return; if (!isFileViewsChanged) return;
console.log('setting file view', {
newInitialFileViews,
fileId,
fileView,
secondaryView
});
onSetFileView({ onSetFileView({
fileId, fileId,
fileView, fileView,

View File

@ -31,9 +31,7 @@ export const useSelectedFile = ({
* @param file * @param file
*/ */
const onSetSelectedFile = useMemoizedFn(async (file: SelectedFile | null) => { const onSetSelectedFile = useMemoizedFn(async (file: SelectedFile | null) => {
console.log('onSetSelectedFile', file);
const handleFileCollapse = shouldCloseSplitter(file, selectedFile, appSplitterRef); const handleFileCollapse = shouldCloseSplitter(file, selectedFile, appSplitterRef);
console.log('handleFileCollapse', handleFileCollapse);
if (file && chatParams.chatId) { if (file && chatParams.chatId) {
const link = assetParamsToRoute({ const link = assetParamsToRoute({
@ -43,8 +41,6 @@ export const useSelectedFile = ({
versionNumber: file?.versionNumber versionNumber: file?.versionNumber
}); });
console.log('link', link);
if (link) onChangePage(link); if (link) onChangePage(link);
} }

View File

@ -47,24 +47,24 @@ export const compareObjectsByKeys = <K extends string>(
if (typeof val1 === 'object' && typeof val2 === 'object') { if (typeof val1 === 'object' && typeof val2 === 'object') {
const itWasEqual = isEqual(val1, val2) || isEqual(JSON.stringify(val1), JSON.stringify(val2)); const itWasEqual = isEqual(val1, val2) || isEqual(JSON.stringify(val1), JSON.stringify(val2));
if (!itWasEqual) { // if (!itWasEqual) {
console.log('--------------NESTED KEYS NOT EQUAL------------------'); // console.log('--------------NESTED KEYS NOT EQUAL------------------');
console.log('KEY', key); // console.log('KEY', key);
console.log('ORIGINAL', val1); // console.log('ORIGINAL', val1);
console.log('NEW', val2); // console.log('NEW', val2);
} // }
return itWasEqual; return itWasEqual;
} }
const itWasEqual = isEqual(val1, val2); const itWasEqual = isEqual(val1, val2);
if (!itWasEqual) { // if (!itWasEqual) {
console.log('--------------KEYS NOT EQUAL------------------'); // console.log('--------------KEYS NOT EQUAL------------------');
console.log('KEY', key); // console.log('KEY', key);
console.log('ORIGINAL', val1); // console.log('ORIGINAL', val1);
console.log('NEW', val2); // console.log('NEW', val2);
} // }
return itWasEqual; return itWasEqual;
}); });