Skip to content

Commit

Permalink
add page feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
yujonglee committed Aug 20, 2024
1 parent fcb484e commit c631a2f
Show file tree
Hide file tree
Showing 18 changed files with 446 additions and 169 deletions.
10 changes: 10 additions & 0 deletions core/lib/canary/analytics/feedback_page.datasource
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
SCHEMA >
`host` String `json:$.host`,
`path` String `json:$.path`,
`score` Int8 `json:$.score`,
`account_id` String `json:$.account_id`,
`fingerprint` String `json:$.fingerprint`,
`timestamp` DateTime `json:$.timestamp`

ENGINE MergeTree
ENGINE_SORTING_KEY fingerprint, timestamp
33 changes: 24 additions & 9 deletions core/lib/canary/analytics/tinybird.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
defmodule Canary.Analytics.Tinybird do
@datasource "web"

defmodule Canary.Analytics do
defp client() do
base_url = Application.get_env(:canary, :tinybird) |> Keyword.fetch!(:base_url)
api_key = Application.get_env(:canary, :tinybird) |> Keyword.fetch!(:api_key)
Expand All @@ -11,11 +9,28 @@ defmodule Canary.Analytics.Tinybird do
)
end

def event(data) do
client()
|> Req.post(
url: "/v0/events?name=#{@datasource}",
json: Map.merge(data, %{"timestamp" => DateTime.to_iso8601(DateTime.utc_now())})
)
def event(source, data) do
result =
client()
|> Req.post(
url: "/v0/events?name=#{source}",
json: data |> Map.merge(%{timestamp: DateTime.to_iso8601(DateTime.utc_now())})
)

case result do
{:ok, %{status: 202, body: %{"quarantined_rows" => rows}}} when rows > 0 ->
{:error, :quarantined}

{:ok, %{status: 202, body: body}} ->
{:ok, body}

error ->
error
end
end
end

defmodule Canary.Analytics.FeedbackPage do
@derive Jason.Encoder
defstruct [:host, :path, :score, :account_id, :fingerprint, :timestamp]
end
13 changes: 0 additions & 13 deletions core/lib/canary/analytics/web.datasource

This file was deleted.

38 changes: 36 additions & 2 deletions core/lib/canary_web/operations_controller.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
defmodule CanaryWeb.OperationsController do
use CanaryWeb, :controller

plug :find_client when action in [:search, :ask]
plug :ensure_valid_host when action in [:search, :ask]
plug :find_client when action in [:search, :ask, :feedback_page]
plug :ensure_valid_host when action in [:search, :ask, :feedback_page]

defp find_client(conn, _opts) do
err_msg = "no client found with the given key"
Expand All @@ -20,6 +20,7 @@ defmodule CanaryWeb.OperationsController do
if Application.get_env(:canary, :env) == :prod and
conn.host not in [
host_url,
"getcanary.dev",
"cloud.getcanary.dev",
"demo.getcanary.dev"
] do
Expand All @@ -29,6 +30,39 @@ defmodule CanaryWeb.OperationsController do
end
end

defp fingerprint(conn) do
ip = to_string(:inet_parse.ntoa(conn.remote_ip))
user_agent = get_req_header(conn, "user-agent") |> List.first()
current_date = Date.utc_today() |> Date.to_string()

:crypto.hash(:md5, ip <> user_agent <> current_date)
|> Base.encode16(case: :lower)
end

def feedback_page(conn, %{"url" => url, "score" => score}) do
%URI{host: host, path: path} = URI.parse(url)

data = %Canary.Analytics.FeedbackPage{
host: host,
path: path,
score: score,
account_id: conn.assigns.client.account.id,
fingerprint: fingerprint(conn)
}

case Canary.Analytics.event("feedback_page", data) do
{:ok, _} ->
conn
|> send_resp(200, "")
|> halt()

error ->
conn
|> send_resp(500, Jason.encode!(%{error: error}))
|> halt()
end
end

def search(conn, %{"query" => query}) do
source = conn.assigns.client.sources |> Enum.at(0)

Expand Down
1 change: 1 addition & 0 deletions core/lib/canary_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ defmodule CanaryWeb.Router do

post "/search", CanaryWeb.OperationsController, :search
post "/ask", CanaryWeb.OperationsController, :ask
post "/feedback/page", CanaryWeb.OperationsController, :feedback_page

forward "/", CanaryWeb.AshRouter
end
Expand Down
18 changes: 10 additions & 8 deletions js/apps/docs/.vitepress/theme/index.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
// import { h } from "vue";
import { h } from "vue";
// import { useRoute } from "vitepress";
import DefaultTheme from "vitepress/theme";
import "./tailwind.css";

import { inject } from "@vercel/analytics";
// import Search from "../../components/CloudSearch.vue";
import Footer from "../../components/Footer.vue";

/** @type {import('vitepress').Theme} */
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
inject();
},
// Layout() {
// const route = useRoute();
// const showSearch = () => /^\/docs/.test(route.path);
Layout() {
// const route = useRoute();
// const isDoc = () => /^\/docs/.test(route.path);

// return h(DefaultTheme.Layout, null, {
// "nav-bar-content-before": () => (showSearch() ? h(Search) : null),
// });
// },
return h(DefaultTheme.Layout, null, {
// "nav-bar-content-before": () => (showSearch() ? h(Search) : null),
"doc-footer-before": () => h(Footer),
});
},
};
22 changes: 22 additions & 0 deletions js/apps/docs/components/Footer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
const loaded = ref(false);
onMounted(() => {
Promise.all([
import("@getcanary/web/components/canary-styles.js"),
import("@getcanary/web/components/canary-feedback-page.js"),
]).then(() => {
loaded.value = true;
});
});
</script>

<template>
<div class="flex justify-center items-center" v-if="loaded">
<canary-styles framework="vitepress">
<canary-feedback-page></canary-feedback-page>
</canary-styles>
</div>
</template>
2 changes: 1 addition & 1 deletion js/apps/docs/components/Headline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<h1 class="text-2xl md:text-4xl">
What if users can
<span class="underline text-yellow-500">find anything</span>
in the docs,
in your docs,
<br />
And you
<span class="underline text-yellow-500">understand everything</span>
Expand Down
33 changes: 33 additions & 0 deletions js/apps/docs/contents/docs/cloud/features/feedback.md
Original file line number Diff line number Diff line change
@@ -1 +1,34 @@
# Feedback

## Per Page

### Vitepress

#### Create Component

```html-vue
<template>
</template>
```

#### Modify Layout

::: code-group

```js [.vitepress/theme/index.js]
import { h } from 'vue' // [!code ++]
import Footer from "<YOUR_COMPONENT_PATH>.vue" // [!code ++]

/** @type {import('vitepress').Theme} */
export default {
...
Layout() { // [!code ++]
return h(DefaultTheme.Layout, null, { // [!code ++]
"doc-footer-before": () => h(Footer), // [!code ++]
}) // [!code ++]
} // [!code ++]
...
};
```

:::
3 changes: 2 additions & 1 deletion js/packages/web/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import "../src/components/canary-root";
import { initialize, mswLoader } from "msw-storybook-addon";
initialize();

import { searchHandler, askHandler } from "../src/msw";
import { searchHandler, askHandler, feedbackPageHandler } from "../src/msw";
import "../src/stories.css";

const preview: Preview = {
Expand Down Expand Up @@ -38,6 +38,7 @@ const preview: Preview = {
handlers: {
search: searchHandler,
ask: askHandler,
feedbackPage: feedbackPageHandler,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { customElement, property } from "lit/decorators.js";

import { MODAL_CLOSE_EVENT } from "./canary-modal";

const NAME = "canary-feedback";
const NAME = "canary-feedback-form";

@customElement(NAME)
export class CanaryFeedback extends LitElement {
export class CanaryFeedbackForm extends LitElement {
@property({ type: String }) title = "Submit feedback";

render() {
Expand Down Expand Up @@ -90,6 +90,6 @@ export class CanaryFeedback extends LitElement {

declare global {
interface HTMLElementTagNameMap {
[NAME]: CanaryFeedback;
[NAME]: CanaryFeedbackForm;
}
}
118 changes: 118 additions & 0 deletions js/packages/web/src/components/canary-feedback-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { LitElement, html, css } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Task } from "@lit/task";

import { withTimeout } from "../utils";

import "./canary-loading-spinner";

const NAME = "canary-feedback-page";

const BASE_URL = "https://cloud.getcanary.dev/api/v1";

@customElement(NAME)
export class CanaryFeedbackPage extends LitElement {
@property({ type: String, attribute: "text-initial" })
initialText = "Was this helpful?";

@property({ type: String, attribute: "text-complete" })
completeText = "Got it, Thank you!";

@property({ type: String })
key = "";

private _task = new Task(this, {
task: async ([url, score]: [string, number], { signal }) => {
const response = await fetch(`${BASE_URL}/feedback/page`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: this.key, url, score }),
signal: withTimeout(signal, 2500),
});

if (!response.ok) {
throw new Error(response.statusText);
}

return null;
},
autoRun: false,
});

render() {
return html`
<div class="container">
${this._task.render({
initial: () => html`
<span class="text">${this.initialText}</span>
<span
class="icon i-heroicons-hand-thumb-up"
@click=${() => this._handleClick(1)}
></span>
<span
class="icon i-heroicons-hand-thumb-down"
@click=${() => this._handleClick(-1)}
></span>
`,
pending: () =>
html` <span class="text">${this.initialText}</span>
<canary-loading-spinner></canary-loading-spinner>`,
complete: () =>
html`<span>${this.completeText}</span>
<span class="i-heroicons-check-circle"></span> `,
error: () => {
console.error(this._task.error);
return html`<div>Sorry, something went wrong.</div> `;
},
})}
</div>
`;
}

private _handleClick(value: number) {
this._task.run([document.location.href, value]);
}

static styles = [
css`
@unocss-placeholder;
`,
css`
.icon {
cursor: pointer;
color: var(--canary-is-light, var(--canary-color-gray-50))
var(--canary-is-dark, var(--canary-color-gray-20));
}
.icon:hover {
color: var(--canary-is-light, var(--canary-color-gray-10))
var(--canary-is-dark, var(--canary-color-gray-0));
}
.text {
margin-right: 4px;
}
.container {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
width: fit-content;
border: 1px solid var(--canary-color-gray-90);
border-radius: 24px;
padding: 8px 12px;
color: var(--canary-color-gray-20);
background-color: var(--canary-is-light, var(--canary-color-gray-100))
var(--canary-is-dark, var(--canary-color-gray-80));
}
`,
];
}

declare global {
interface HTMLElementTagNameMap {
[NAME]: CanaryFeedbackPage;
}
}
Loading

0 comments on commit c631a2f

Please sign in to comment.