Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customer API endpoint doesn't update metadata #995

Closed
CharlesMangwa opened this issue Nov 22, 2021 · 11 comments
Closed

Customer API endpoint doesn't update metadata #995

CharlesMangwa opened this issue Nov 22, 2021 · 11 comments
Labels
bug Something isn't working

Comments

@CharlesMangwa
Copy link

Describe the bug
After following the docs to update a customer, the metadata is always returned as null.

To Reproduce
Steps to reproduce the behavior:

  1. Create an API key in the dashboard
  2. Add your API key as a Bearer token & make a GET request to retrieve any customer id via https://app.papercups.io/api/v1/customers/
  3. With that id, make a PUT request to https://app.papercups.io/api/v1/customers/[id]/ with a body, ie:
{
   "customer":{
      "metadata":{
         "random_custom_field":"034c0f85-0be3-4086-acf8-85ed8f4f1228"
      }
   }
}
  1. You get a 200 but data.metadata is still null

Expected behavior
data.metadata would return the provided "random_custom_field": "034c0f85-0be3-4086-acf8-85ed8f4f1228",.

@jeepers3327
Copy link
Contributor

I believe you should use PUT request on https://app.papercups.io/api/v1/customers/[id]/metadata as described in the router.ex file

put("/customers/:id/metadata", CustomerController, :update_metadata)

The reason https://app.papercups.io/api/v1/customers/[id]/ returns a null is because the changeset used in update_customer does not pick up the metadata key.

def update_customer(%Customer{} = customer, attrs) do
customer
|> Customer.changeset(attrs)
|> Repo.update()
end

def changeset(customer, attrs) do
customer
|> cast(attrs, [
:first_seen,
:account_id,
:company_id,
:email,
:name,
:phone,
:external_id,
:profile_photo_url,
:browser,
:browser_version,
:browser_language,
:os,
:ip,
:last_seen_at,
:current_url,
:host,
:pathname,
:screen_height,
:screen_width,
:lib,
:time_zone
])
|> validate_required([:first_seen, :last_seen_at, :account_id])
|> foreign_key_constraint(:account_id)
|> foreign_key_constraint(:company_id)
end

But when you look at changeset used in Customers.update_customer_metadata it picks up the metadata key.

def metadata_changeset(customer, attrs) do
customer
|> cast(attrs, [
:metadata,
:email,
:name,
:phone,
:external_id,
:browser,
:browser_version,
:browser_language,
:os,
:ip,
:last_seen_at,
:current_url,
:host,
:pathname,
:screen_height,
:screen_width,
:lib,
:time_zone
])
end

@CharlesMangwa
Copy link
Author

Thanks for the lead @jeepers3327! I forgot to mention in my initial description that I did try a PUT on https://app.papercups.io/api/v1/customers/[id]/metadata but it returns

{
  "errors": {
    "detail": "Not Found"
  }
}

even though the id I provided does exist and all. Am I missing something obvious?

FWIW: I also tried https://app.papercups.io/api/customers/[id]/metadata (without the v1). This one does return the data object but metadata is still null.

@jeepers3327
Copy link
Contributor

jeepers3327 commented Dec 3, 2021

Hi @CharlesMangwa,
Could you try accessing this endpoint get("/customers/:id/exists", CustomerController, :exists) with your id and tell me the result if it exist or not. Based on https://github.com/papercups-io/papercups/blob/master/lib/chat_api_web/router.ex, it shouldn't matter whether there's a v1 prefix on the api endpoint or not.

Note: I don't really launch the application on my local or the hosted version, I just give out advice based on the code.

@CharlesMangwa
Copy link
Author

hey @jeepers3327, thanks for your help, it's much appreciated!

There seem to be different scopes actually:

scope "/api", ChatApiWeb do

scope "/api/v1", ChatApiWeb do

with only the former accepting put("/customers/:id/metadata"):

put("/customers/:id/metadata", CustomerController, :update_metadata)

That could explain why trying to access /api/v1/customers/[id]/exists only opens up the Papercups dashboard but /api/customers/[id]/exists does return

{
  "data": true
}

as /exists is defined in the /api scope only:

get("/customers/:id/exists", CustomerController, :exists)

@jeepers3327
Copy link
Contributor

Have you checked if metadata field was filled when you update it or does it nullify the current metadata value

@CharlesMangwa
Copy link
Author

There's no metadata (null) and I'm trying to add some but it always returns null

@jeepers3327
Copy link
Contributor

Hi @CharlesMangwa,

I've found the bug and how to fix it, though I need to ask for confirmation from @reichert621 regarding the fix.

From line 131 - 142, it fetches metadata from the PUT request, merges it with extra fields and then transform some of fields after sanitizing it.

def update_metadata(conn, %{"id" => id, "metadata" => metadata}) do
# TODO: include account_id
customer = Customers.get_customer!(id)
updates =
metadata
|> Map.merge(%{
"ip" =>
if ip_collection_enabled?() do
conn.remote_ip |> :inet_parse.ntoa() |> to_string()
else
nil
end,
"last_seen_at" => DateTime.utc_now()
})
|> Customers.sanitize_metadata()
with {:ok, %Customer{} = customer} <- Customers.update_customer_metadata(customer, updates) do

Based from the line map transformation the given map below (came from PUT request):

{
    "metadata": {
        "social handle": "mikoto"
    }
}

will become

%{
  "ip" => nil,
  "last_seen_at" => ~U[2021-12-04 11:11:38.308617Z],
  "social handle" => "mikoto"
}

and since updating a metadata calls Customers.update_customer_metadata function, the Customer.metadata_changeset will also be called, processing the merged map. Now, since metadata key is no longer present upon calling the changeset it becomes null

def metadata_changeset(customer, attrs) do
customer
|> cast(attrs, [
:metadata,
:email,
:name,
:phone,
:external_id,
:browser,
:browser_version,
:browser_language,
:os,
:ip,
:last_seen_at,
:current_url,
:host,
:pathname,
:screen_height,
:screen_width,
:lib,
:time_zone
])

Fix

The fix is actually simple, instead of

def sanitize_metadata(metadata) do
metadata
|> sanitize_metadata_email()
|> sanitize_metadata_external_id()
|> sanitize_metadata_current_url()
|> sanitize_ad_hoc_metadata()
end

I make it like this

  def sanitize_metadata(metadata) do
    metadata
    |> sanitize_metadata_email()
    |> sanitize_metadata_external_id()
    |> sanitize_metadata_current_url()
    |> sanitize_ad_hoc_metadata()
    |> prepare_metadata()
  end

  def prepare_metadata(metadata) when is_map(metadata), do: %{metadata: metadata}
  def prepare_metadata(metadata), do: metadata

After applying the change, here's what Ecto.Changeset prints out.

#Ecto.Changeset<
  action: nil,
  changes: %{
    metadata: %{
      "ip" => nil,
      "last_seen_at" => ~U[2021-12-04 11:18:05.094519Z],
      "social handle" => "mikoto"
    }
  },
  errors: [],
  data: #ChatApi.Customers.Customer<>,
  v

@CharlesMangwa
Copy link
Author

Hey @jeepers3327! Do you think you could put up a PR with this fix pls?

@jeepers3327
Copy link
Contributor

My apologies @CharlesMangwa for delay PR , I've been away for about a month due to unforeseen events.

@CharlesMangwa
Copy link
Author

No worries @jeepers3327! Take care of yourself first, the PR can wait 🙌

@CharlesMangwa
Copy link
Author

Closed by #1000 (comment).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants