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

feat: various social image improvements #270

Merged
merged 1 commit into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions packages/otelbin/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@
###### REQUIRED ENV VARS ######
###############################

# URL origin under which the app is available. Used as the hostname for short links
# Example: http://localhost:3000
DEPLOYMENT_ORIGIN=

# Upstash Redis – required for Redis caching and short link persistence
# Get your Redis REST URL and Token here: https://upstash.com/docs/redis/overall/getstarted
UPSTASH_REDIS_REST_URL=
Expand Down
11 changes: 0 additions & 11 deletions packages/otelbin/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion packages/otelbin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.14.201",
"@types/node": "^18.18.13",
"@types/prettier": "^3.0.0",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^5.59.6",
Expand Down
2 changes: 2 additions & 0 deletions packages/otelbin/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
User-agent: *
Allow: /
2 changes: 1 addition & 1 deletion packages/otelbin/src/app/og/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const Node = ({ data, icon, type }: { data: IData; icon: React.ReactNode; type:
<div style={customNodeStyles} tw="flex-col">
<div
tw={`flex w-full flex-col items-center justify-center px-2 ${
splitLabel[1] && splitLabel[1].length > 0 && "mt-[2px]"
splitLabel[1] && splitLabel[1].length > 0 ? "mt-[2px]" : ""
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This caused warnings in the logic because of an unknown tailwind utility called undefined

}`}
>
<div style={iconColor}>{isConnector ? <ConnectorIcon /> : icon}</div>
Expand Down
6 changes: 3 additions & 3 deletions packages/otelbin/src/app/og/ParentsNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ const ParentsNode = ({ nodeData, nodes }: { nodeData: Node; nodes?: Node[] }) =>
key={node.id}
d={
side === "left"
? `M10 ${(idx + 1) * (height / nodesCount) - 75}
C 20,${(idx + 1) * (height / nodesCount) - 75},35,${height / 2 - 25}
? `M10 ${(idx + 1) * (height / nodesCount) - 75}
C 20,${(idx + 1) * (height / nodesCount) - 75},35,${height / 2 - 25}
50 ${height / 2 - 25}`
: `M10 ${height / 2 - 25}
C 20,${height / 2 - 25},35,${(idx + 1) * (height / nodesCount) - 75}
C 20,${height / 2 - 25},35,${(idx + 1) * (height / nodesCount) - 75}
50 ${(idx + 1) * (height / nodesCount) - 75}`
}
stroke="#FFFFFF"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import { getShortLinkPersistenceKey } from "~/lib/shortLink";
import type { IConfig } from "~/components/react-flow/dataType";
import { editorBinding } from "~/components/monaco-editor/editorBinding";
import JsYaml from "js-yaml";
import { calcScale, toUrlState } from "../../metadataUtils";
import Logo from "../../../../components/assets/svg/otelbin_logo_white.svg";
import { calcScale, toUrlState } from "../metadataUtils";
import Logo from "~/components/assets/svg/otelbin_logo_white.svg";
import { notFound } from "next/navigation";

export const runtime = "edge";

Expand All @@ -20,7 +21,7 @@ const edgeWidth = 80;
export async function GET(request: NextRequest) {
const shortLinkId = request.nextUrl.searchParams.get("id") ?? "";
if (!shortLinkId) {
return new NextResponse("No short link ID found in the request", { status: 400 });
return notFound();
}
const fullLink = (await redis.get<string>(getShortLinkPersistenceKey(shortLinkId))) ?? "";
let url;
Expand Down Expand Up @@ -75,7 +76,7 @@ export async function GET(request: NextRequest) {
{
width: 1200,
height: 630,
headers: { "Cache-Control": "public, max-age=3600, stale-while-revalidate=3600, stale-if-error=3600" },
headers: { "Cache-Control": "public, max-age=3600, stale-while-revalidate=604800, stale-if-error=604800" },
}
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "@jest/globals";
import { sortAndDeduplicate, extractComponents, calcScale, toUrlState } from "./metadataUtils";
import { calcScale, extractComponents, sortAndDeduplicate, toUrlState } from "./metadataUtils";
import type { IConfig } from "~/components/react-flow/dataType";
import { type Node } from "reactflow";
import { editorBinding } from "../../components/monaco-editor/editorBinding";
import { editorBinding } from "../../../components/monaco-editor/editorBinding";

describe("sortAndDeduplicate", () => {
it("should sort and deduplicate an array of strings and return a comma separated string of components", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import type { IConfig } from "~/components/react-flow/dataType";
import { type Node } from "reactflow";
import type { Binding } from "~/lib/urlState/binding";
import { parseUrlState } from "../../lib/urlState/parseUrlState";
import { parseUrlState } from "../../../lib/urlState/parseUrlState";
import type { Bindings } from "~/lib/urlState/typeMapping";

export function sortAndDeduplicate(arr: string[]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { IConfig } from "~/components/react-flow/dataType";
import { getShortLinkPersistenceKey } from "~/lib/shortLink";
import { editorBinding } from "~/components/monaco-editor/editorBinding";
import JsYaml from "js-yaml";
import { extractComponents, sortAndDeduplicate, toUrlState } from "../metadataUtils";
import { extractComponents, sortAndDeduplicate, toUrlState } from "~/app/s/[id]/metadataUtils";
import { notFound } from "next/navigation";

interface ExtendedMetadata {
Expand Down Expand Up @@ -38,7 +38,7 @@ export async function generateMetadata({ params }: { params: { id: string } }):
return notFound();
}
const url = new URL(fullLink);
const imagesUrl = new URL(`/social-preview/${params.id}/img`, url.origin);
const imagesUrl = new URL(`/s/${params.id}/img`, url.origin);
const { config } = toUrlState(url, [editorBinding]);
const jsonData = JsYaml.load(config) as IConfig;
const components = extractComponents(jsonData);
Expand Down
8 changes: 2 additions & 6 deletions packages/otelbin/src/app/s/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ import { Redis } from "@upstash/redis";

const redis = Redis.fromEnv();

export async function GET(request: Request, { params }: { params: { id: string } }) {
export async function GET(_: Request, { params }: { params: { id: string } }) {
const shortLink = await redis.get<string>(getShortLinkPersistenceKey(params.id));
return NextResponse.redirect(shortLink || "/", {
headers: {
"Cache-Control": "public, max-age=3600, stale-while-revalidate=3600, stale-if-error=3600",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can actually cause issues without the correct Vary response header. Hence caching is removed for this code path.

},
});
return NextResponse.redirect(shortLink || "/");
}
10 changes: 5 additions & 5 deletions packages/otelbin/src/app/s/new/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Redis } from "@upstash/redis";
import * as crypto from "crypto";
import { Ratelimit } from "@upstash/ratelimit";
import { type NextRequest, NextResponse } from "next/server";
import * as process from "process";
import { getShortLinkPersistenceKey } from "~/lib/shortLink";
import { getUserIdentifier } from "~/lib/userIdentifier";

Expand Down Expand Up @@ -55,13 +54,14 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
);
}

const uuid = crypto.randomUUID();
await redis.set(getShortLinkPersistenceKey(uuid), longURL);
const id = crypto.createHash("sha1").update(longURL).digest("hex");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ensures that there are fewer short link permutations for the same data.

await redis.set(getShortLinkPersistenceKey(id), longURL);

const shortURL = `${process.env.DEPLOYMENT_ORIGIN}/s/${uuid}`;
const shortURL = new URL(`/s/${id}`, request.nextUrl.origin);
return NextResponse.json(
{
shortLink: shortURL,
shortLink: shortURL.href,
imgURL: `${shortURL.href}/img`,
},
{
headers: {
Expand Down
75 changes: 57 additions & 18 deletions packages/otelbin/src/components/share/ShareContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { SignInButton, useAuth } from "@clerk/nextjs";
import { Button } from "~/components/button";
import { ArrowDownToLine, Copy, LogIn } from "lucide-react";
import { ArrowDownToLine, Copy, Loader2, LogIn } from "lucide-react";
import { useUrlState } from "~/lib/urlState/client/useUrlState";
import { editorBinding } from "~/components/monaco-editor/editorBinding";
import { UrlCopy } from "~/components/share/UrlCopy";
Expand All @@ -21,7 +21,7 @@ export function ShareContent() {
let fullURL = window.location.origin + getLink({});
const { isSignedIn } = useAuth();

const { data } = useSWRImmutable<{ shortLink: string }>(
const { data } = useSWRImmutable<{ shortLink: string; imgURL: string }>(
isSignedIn
? {
url: `/s/new`,
Expand All @@ -47,28 +47,58 @@ export function ShareContent() {
</SignInButton>
)}

{isSignedIn && (
<div className="mt-3 border-t-1 border-subtle px-4 pt-3">
<p className="weight mb-2 text-sm font-normal text-default">Link to this configuration with a badge.</p>
{!data?.shortLink && isSignedIn && (
<em className="mx-4 mt-3 mb-0 text-sm font-normal text-default flex items-center gap-2">
<Loader2 className="motion-safe:animate-spin" size={16} /> Creating short URL…
</em>
)}

{data?.shortLink && (
<>
<div className="mt-3 border-t-1 border-subtle px-4 pt-1">
<p className="weight mb-2 text-sm font-normal text-default flex items-center justify-between">
Link to this configuration with a badge.
<Tooltip>
<TooltipTrigger asChild>
<IconButton size="xs" onClick={copyMarkdownToClipboard}>
<Copy />
</IconButton>
</TooltipTrigger>
<TooltipContent>Copy badge markdown to clipboard</TooltipContent>
</Tooltip>
</p>

<div className="flex gap-2 items-center">
<img
src="/badges/collector-config"
alt="OpenTelemetry collector configuration on OTelBin"
width={167}
height={20}
className="max-h-[20px] grow-0"
/>
<Tooltip>
<TooltipTrigger asChild>
<IconButton size="xs" onClick={copyToClipboard}>
<Copy />
</IconButton>
</TooltipTrigger>
<TooltipContent>Copy badge markdown to clipboard</TooltipContent>
</Tooltip>
</div>
</div>

<div className="mt-3 border-t-1 border-subtle px-4 pt-1">
<p className="weight mb-2 text-sm font-normal text-default flex items-center justify-between">
Link to an image of this visualization.
<Tooltip>
<TooltipTrigger asChild>
<IconButton size="xs" onClick={copyImageUrlToClipboard}>
<Copy />
</IconButton>
</TooltipTrigger>
<TooltipContent>Copy image URL to clipboard</TooltipContent>
</Tooltip>
</p>

<img
src={data.imgURL}
width={280}
height={147}
alt="OpenTelemetry collector configuration on OTelBin"
className="grow-0"
/>
</div>
</>
)}

<div className="mt-4 border-t-1 border-subtle px-4 py-3">
Expand All @@ -87,12 +117,22 @@ export function ShareContent() {
</div>
);

function copyToClipboard() {
function copyMarkdownToClipboard() {
const text = `[![OpenTelemetry collector configuration on OTelBin](${
window.location.origin + "/badges/collector-config"
})](${fullURL})`;
copyToClipboard(text);
track("Copied badge markdown");
}

function copyImageUrlToClipboard() {
copyToClipboard(data?.imgURL ?? "");
track("Copied image URL");
}

function copyToClipboard(value: string) {
navigator.clipboard
.writeText(text)
.writeText(value)
.then(() => {
toast({
description: "Copied to clipboard.",
Expand All @@ -104,6 +144,5 @@ export function ShareContent() {
description: "Failed to copy to clipboard",
});
});
track("Copied badge markdown");
}
}
62 changes: 23 additions & 39 deletions packages/otelbin/src/lib/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,46 +6,30 @@ import { isBotRequest } from "./utils";
import { NextRequest } from "next/server";

describe("isBotRequest", () => {
const createMockRequest = (url: string, userAgent?: string) => {
const mockRequest = new Request(url);
const mockNextRequest = new NextRequest(mockRequest, {});
it.each([
["https://example.com", "Slackbot 1.0 (+https://api.slack.com/robots)", true],
["https://example.com", "Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)", true],
["https://example.com", "Slack-ImgProxy (+https://api.slack.com/robots)", true],
[
"https://example.com",
"LinkedInBot/1.0 (compatible; Mozilla/5.0; Apache-HttpClient +http://www.linkedin.com)",
true,
],
[
"https://example.com",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
false,
],
[
"https://example.com?bot=true",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
true,
],
])("should identify requests to %s by %s as bot=%s", (url, userAgent, isBot) => {
const req = new NextRequest(new Request(url), {});
if (userAgent) {
mockNextRequest.headers.set("User-Agent", userAgent);
req.headers.set("User-Agent", userAgent);
}
return mockNextRequest;
};

it("should return true for user-agent bot requests", () => {
const mockNextRequest = createMockRequest(
"https://www.whatever.com/admin/check-it-out",
"Slackbot 1.0 (+https://api.slack.com/robots)"
);
const result = isBotRequest(mockNextRequest);

expect(result).toBe(true);
});

it("should return true for url param ?bot=true requests", () => {
const mockNextRequest = createMockRequest("https://www.whatever.com/admin/check-it-out?bot=true");
const result = isBotRequest(mockNextRequest);

expect(result).toBe(true);
});

it("should return true for url param ?bot=false requests", () => {
const mockNextRequest = createMockRequest("https://www.whatever.com/admin/check-it-out?bot=false");
const result = isBotRequest(mockNextRequest);

expect(result).toBe(true);
});

it("should return false for non-bot requests", () => {
const mockNextRequest = createMockRequest(
"https://www.whatever.com/admin/check-it-out",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.3029.110 Safari/537"
);
const result = isBotRequest(mockNextRequest);

expect(result).toBe(false);
expect(isBotRequest(req)).toBe(isBot);
});
});
Loading
Loading