Skip to content

Commit c6061de

Browse files
committed
init
1 parent 945eb71 commit c6061de

File tree

5 files changed

+203
-0
lines changed

5 files changed

+203
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Local Netlify folder
2+
.netlify

index.html

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Document</title>
7+
<script src="main.js" type="module"></script>
8+
</head>
9+
<body>
10+
<h1>Netlify Logs</h1>
11+
<input type="text" id="deployId" placeholder="Deploy ID" />
12+
<input type="text" id="siteId" placeholder="Site ID" />
13+
<button id="connect">Connect</button>
14+
<div id="logs"></div>
15+
</body>
16+
</html>

main.js

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
let cachedAccessControlToken = null;
2+
3+
async function getAccessControlToken() {
4+
if (cachedAccessControlToken) {
5+
return cachedAccessControlToken;
6+
}
7+
8+
const tokenResp = await fetch(".netlify/functions/generate-token", {
9+
credentials: "include",
10+
});
11+
12+
if (tokenResp.status !== 200) {
13+
cachedAccessControlToken = null;
14+
throw new Error("failed to access control token for user");
15+
}
16+
17+
const { accessControlToken } = await tokenResp.json();
18+
19+
cachedAccessControlToken = accessControlToken;
20+
21+
return accessControlToken;
22+
}
23+
24+
class NetlifyLogsService {
25+
constructor(options = {}) {
26+
this.options = options;
27+
this.logs = [];
28+
this.shouldReconnect = true;
29+
// this.ws = this.connect();
30+
}
31+
32+
destroy() {
33+
this.shouldReconnect = false;
34+
window.clearInterval(this.reconnectTimeout);
35+
this.notifyLogsUpdated.cancel();
36+
this.ws.close();
37+
}
38+
39+
connect(options) {
40+
this.ws = new WebSocket("wss://socketeer.services.netlify.com/build/logs");
41+
this.ws.addEventListener("open", () => {
42+
getAccessControlToken()
43+
.then((accessToken) => {
44+
this.ws.send(
45+
JSON.stringify({
46+
deploy_id: options.deployId || this.options.deployId,
47+
site_id: options.siteId || this.options.siteId,
48+
access_token: accessToken,
49+
})
50+
);
51+
})
52+
.catch((error) => {
53+
console.error(
54+
"NetlifyLogsService failed to get access control token",
55+
error
56+
);
57+
});
58+
});
59+
this.ws.addEventListener("message", (event) => {
60+
try {
61+
const data = JSON.parse(event.data);
62+
const ts = new Date(data.ts).getTime();
63+
64+
if (data.type === "error") {
65+
throw data;
66+
}
67+
68+
this.logs ??= [];
69+
this.logs.push({
70+
id: `${ts}${this.logs.length}`,
71+
timestamp: ts,
72+
message: data.message,
73+
});
74+
this.notifyLogsUpdated();
75+
} catch (e) {
76+
if (e?.type === "error" && e.status === 401) {
77+
console.error("NetlifyLogsService no permission");
78+
this.options.onForbidden?.();
79+
return;
80+
}
81+
console.error(`NetlifyLogsService can't decode socket message`, e);
82+
}
83+
});
84+
this.ws.addEventListener("close", () => {
85+
console.info(`NetlifyLogsService socket closed`);
86+
if (this.shouldReconnect) {
87+
this.reconnectTimeout = window.setTimeout(
88+
() => this.connect(),
89+
this.options.reconnect ?? 1000
90+
);
91+
}
92+
});
93+
this.ws.addEventListener("error", (error) => {
94+
console.error(`NetlifyLogsService socket got error`, error);
95+
this.ws.close();
96+
});
97+
return this.ws;
98+
}
99+
100+
notifyLogsUpdated = (function () {
101+
let timeout;
102+
return function () {
103+
clearTimeout(timeout);
104+
timeout = setTimeout(() => {
105+
this.options.onLogsUpdated?.([...(this.logs ?? [])]);
106+
}, 250);
107+
};
108+
})();
109+
}
110+
111+
const logsService = new NetlifyLogsService({
112+
deployId: "your-deploy-id",
113+
siteId: "your-site-id",
114+
onLogsUpdated: (logs) => {
115+
// Handle updated logs
116+
},
117+
onForbidden: () => {
118+
// Handle forbidden access
119+
},
120+
reconnect: 2000, // Optional reconnect timeout in ms
121+
});
122+
123+
// Initialize the logs service
124+
const netlifyLogs = new NetlifyLogsService({
125+
onLogsUpdated: (logs) => {
126+
// Update UI with new logs
127+
const logContainer = document.querySelector("#logs");
128+
if (logContainer) {
129+
logContainer.innerHTML = logs
130+
.map(
131+
(log) => `
132+
<div class="log-entry">
133+
<span class="timestamp">${new Date(
134+
log.timestamp
135+
).toLocaleTimeString()}</span>
136+
<span class="message">${log.message}</span>
137+
</div>
138+
`
139+
)
140+
.join("");
141+
}
142+
},
143+
onForbidden: () => {
144+
console.error("Access forbidden - please check your credentials");
145+
// Optionally show error message to user
146+
alert("Unable to access logs - permission denied");
147+
},
148+
reconnect: 3000,
149+
});
150+
151+
document.getElementById("connect").addEventListener("click", () => {
152+
const deployId = document.getElementById("deployId").value;
153+
const siteId = document.getElementById("siteId").value;
154+
netlifyLogs.connect({ deployId, siteId });
155+
});

netlify/functions/generate-token.mjs

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export default async (req, context) => {
2+
console.log(123);
3+
try {
4+
const response = await fetch(
5+
"https://app.netlify.com/access-control/generate-access-control-token",
6+
{
7+
method: "GET",
8+
headers: {
9+
"Content-Type": "application/json",
10+
Authorization: `Bearer ${process.env.NETLIFY_ACCESS_CONTROL_TOKEN}`,
11+
},
12+
}
13+
);
14+
const data = await response.json();
15+
console.log(data);
16+
return new Response(JSON.stringify(data));
17+
} catch (error) {
18+
console.error(error);
19+
return new Response("Error generating token", { status: 500 });
20+
}
21+
};

package.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "netlify-logs",
3+
"version": "1.0.0",
4+
"description": "A simple tool to view Netlify logs",
5+
"main": "index.html",
6+
"scripts": {
7+
"start": "netlify serve"
8+
}
9+
}

0 commit comments

Comments
 (0)