Erzincan'da “Teknoloji Konuşmaları” konferansı düzenlendi

Sanayi ve Teknoloji Bakan Yardımcısı Çetin Ali Dönmez, Erzincan’da düzenlenen ‘Teknoloji Konuşmaları’ konferansında Milli Teknoloji Hamlesi, girişimcilik ve yapay zeka konularında öğrencilerle buluştu. Savunma sanayisinde yerlilik oranının yüzde 80’i aştığı vurgulandı.

My Watchlist: From 70s Basements to Victorian Crime Scenes

Originally published at harshit.cloud on 2025-01-12.

When I’m not wrestling with Kubernetes clusters or debugging infrastructure at 2 AM, you’ll find me binging shows that either make me laugh or make me think. Here’s my watchlist confession.

The Sitcom Holy Trinity

That 70’s Show

There’s something timeless about a bunch of teenagers sitting in a basement, roasting each other. The circle scenes, Red’s threats about putting his foot somewhere uncomfortable, and Kelso’s stupidity that somehow loops back to genius. It’s comfort TV at its finest.

Arrested Development

“I’ve made a huge mistake.” Haven’t we all? This show rewards rewatching like no other. The layered jokes, the callbacks, the narrator’s deadpan delivery – it’s a masterclass in comedy writing. The Bluth family dysfunction hits different when you’ve seen enough corporate chaos yourself.

How I Met Your Mother (HIMYM)

Yes, I have opinions about the finale. But everything before that? Legendary. Barney’s suits, Marshall and Lily’s relationship goals, and Ted’s endless romantic optimism somehow never got old. The slap bet alone deserves its own appreciation post.

The Detective Obsession

I’m an avid detective movie and show enthusiast. Something about watching brilliant minds piece together impossible puzzles scratches an itch that debugging production issues just can’t reach.

Sherlock

Benedict Cumberbatch’s Sherlock is peak detective content. The mind palace sequences, the rapid-fire deductions, the chemistry with Watson – it redefined what a detective adaptation could be. “The game is on” still gives me chills.

Detective Conan

Shinichi Kudo trapped in a kid’s body, solving murders that somehow happen everywhere he goes. The Japanese detective anime that’s been running since 1996 and I’m still invested. The Black Organization arc is chef’s kiss.

High Potential

This one’s newer, but it has a lot of potential (pun intended). The premise of a high-IQ cleaning lady solving crimes that stump detectives? Count me in. It’s fresh, it’s fun, and it’s finding its groove.

The Common Thread

Whether it’s a sitcom or a detective thriller, I gravitate toward smart writing. Shows that respect their audience, plant seeds that pay off later, and create characters you’d want to grab a beer with (or solve crimes with).

What’s on your watchlist? Always looking for recommendations – especially if it involves either a laugh track or a magnifying glass.

GitHub Actions vs GitLab CI: a practical comparison

Originally published at harshit.cloud on 2024-12-20.

GitHub Actions vs GitLab CI: a practical comparison

Two years, 50 microservices, two CI platforms running side by side. Some repos on GitHub, some on GitLab, same team writing the YAML for both. Here is what stuck after the marketing slides wore off.

syntax and configuration

GitHub Actions

name: CI Pipeline
on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm test

The YAML is readable, the marketplace has an action for almost everything, and matrix builds are a single block. The nesting gets verbose once you have reusable workflows, and environment variable precedence is its own small religion.

GitLab CI

stages:
  - test
  - build

test:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm test
  only:
    - main
    - merge_requests

Flatter than GitHub’s nesting, Docker is a first-class citizen, and the stages concept maps cleanly to how you think about a pipeline. There is no marketplace, so reusable components come from include: files and Docker images you assemble yourself.

performance and speed

build times

A typical Node.js app on our setup builds in 3 to 5 minutes on GitHub Actions and 4 to 6 minutes on GitLab CI. Close enough that I never picked a platform on speed alone.

parallelization

Both handle parallel jobs well. GitHub Actions has cleaner syntax for matrix builds:

strategy:
  matrix:
    node-version: [18, 20, 22]
    os: [ubuntu-latest, windows-latest]

GitLab requires more manual setup for the same result.

ecosystem and marketplace

GitHub Actions marketplace

Over 20,000 actions, and the caching one is the example I keep coming back to:

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

One block, content-addressed cache keyed off the lockfile. The first time you delete the manual cache logic you wrote for GitLab and replace it with this, you feel it.

GitLab’s approach

GitLab does not have a marketplace. You write scripts or use Docker images:

test:
  image: node:20
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
  script:
    - npm ci
    - npm test

More control, but more work.

docker integration

GitLab CI wins here

GitLab CI was built with Docker in mind:

build:
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t myapp .
    - docker push myapp

It just works. No weird permissions issues.

GitHub Actions

Needs more setup for Docker.

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3
- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: myapp:latest

Works fine, but requires more marketplace actions.

secrets management

GitHub

env:
  API_KEY: ${{ secrets.API_KEY }}

Simple. Secrets are org/repo scoped. Works well.

GitLab

variables:
  API_KEY: $CI_DEPLOY_TOKEN

More flexible with group-level variables and environments. Better for complex setups.

cost

GitHub Actions gives private repos 2,000 minutes/month on the free tier, public repos are unlimited, and overage is $0.008/minute. GitLab SaaS gives 400 minutes/month free and charges $10 per 1,000 additional minutes, but self-hosted runners are unlimited. If you can run your own runners, GitLab gets cheaper fast at scale. If you can’t, GitHub’s free tier outlasts it.

self-hosted runners

GitHub

./config.sh --url https://github.com/org/repo --token TOKEN
./run.sh

Setup is straightforward. Runners are repo or org-scoped.

GitLab

gitlab-runner register
gitlab-runner run

More flexible. Can be project, group, or instance-wide. Better for large organizations.

debugging experience

GitHub Actions has clear, searchable logs, lets you re-run individual jobs, and exposes a debug mode behind two secrets. You can SSH into a runner via a third-party action, but it is not a native feature.

GitLab is the one I reach for when a pipeline is genuinely stuck. The log viewer is good, individual job retries are good, but the real difference is interactive debugging. SSH into the runner mid-job, or open a web terminal from the failed job in your browser, and poke at the filesystem while the build is still alive. The first time you do this on a Docker-in-Docker failure that only repros on CI, you stop missing it everywhere else.

when to pick which

GitHub Actions wins when you are already on GitHub, want the marketplace, and your pipelines are small to medium. GitLab CI wins when your Docker workflows are non-trivial, your runner fleet is large, your deployment strategies are gnarly, or you need to debug pipelines without a redeploy loop.

my setup

I use both. GitHub Actions for open-source and frontend, GitLab CI for infrastructure code and the deployments that involve five stages and a manual approval.

common pitfalls

GitHub Actions has a 6-hour hosted-runner job timeout, a 90-day artifact retention default (configurable up to 400 days for public repos, 90 for private), and tight concurrent-job limits on the free tier. Plan around them or pay.

GitLab’s shared runners get sluggish at peak, Docker builds need docker:dind as a service container, and CI/CD variable precedence has at least six rules you will need to read twice. The one that bites me most: project-level variables silently override group-level ones with the same name.

migration tips

GitHub to GitLab

# GitHub
- uses: actions/checkout@v4

# GitLab equivalent
git clone $CI_REPOSITORY_URL
cd $CI_PROJECT_NAME

GitLab to GitHub

Most scripts translate directly. The win is collapsing a few of them into marketplace actions you no longer have to maintain.

Starting fresh, pick whichever platform already hosts your code. The integration tax of running CI on the other vendor outweighs every syntax preference in this post. Whichever one you pick, the only investment that pays back is making the pipeline fast. A slow CI is worse than no CI; it just costs more to ignore.

Git Interactive Rebase: Clean Up Your Commit History

Originally published at harshit.cloud on 2024-12-19.

TIL: Git Interactive Rebase: Clean Up Your Commit History

Discovered git rebase -i today. Turns five “fix typo” commits into one clean commit before the PR review notices.

the problem

My commit history looked like this:

fix typo
WIP
fix typo again
actually fixed it
remove console.log

Not exactly professional for a PR review.

the solution

Interactive rebase lets you edit, squash, and reorder commits:

# Rebase last 5 commits
git rebase -i HEAD~5

# Or rebase everything since branching from main
git rebase -i main

This opens your editor with:

pick 1a2b3c4 fix typo
pick 5d6e7f8 WIP
pick 9g0h1i2 fix typo again
pick 3j4k5l6 actually fixed it
pick 7m8n9o0 remove console.log

# Commands:
# p, pick = use commit
# r, reword = use commit, but edit message
# e, edit = use commit, but stop for amending
# s, squash = meld into previous commit
# f, fixup = like squash, but discard commit message
# d, drop = remove commit

my workflow

Change it to:

pick 1a2b3c4 fix typo
fixup 5d6e7f8 WIP
fixup 9g0h1i2 fix typo again
fixup 3j4k5l6 actually fixed it
fixup 7m8n9o0 remove console.log

Result: one clean commit.

pro tips

Reword commit messages:

pick 1a2b3c4 fix typo
reword 5d6e7f8 add user authentication

Reorder commits:

pick 3j4k5l6 add tests
pick 1a2b3c4 add feature

Split a commit:

edit 1a2b3c4 huge commit with multiple changes

Then:

git reset HEAD^
git add file1.js
git commit -m "feat: add feature A"
git add file2.js
git commit -m "feat: add feature B"
git rebase --continue

warning

Never rebase commits that have been pushed to a shared branch! You’ll rewrite history and cause conflicts for your team.

Safe:

# Your feature branch, not pushed yet
git rebase -i main

Dangerous:

# Main branch that others use
git checkout main
git rebase -i HEAD~5  # DON'T DO THIS!

useful aliases

Add to your ~/.gitconfig:

[alias]
    # Interactive rebase with the given number of latest commits
    rb = "!f() { git rebase -i HEAD~$1; }; f"

    # Rebase on main
    rbm = "!git fetch origin main && git rebase -i origin/main"

Now you can do:

git rb 5        # Rebase last 5 commits
git rbm         # Rebase on main

This has saved me so much embarrassment in code reviews.

Prometheus and Grafana: from zero to production monitoring

Originally published at harshit.cloud on 2024-12-18.

Prometheus and Grafana: from zero to production monitoring

We started shipping monitoring after a string of outages where customers paged us before our own dashboards did. This is the stack we landed on, written like you’re standing it up tomorrow.

why Prometheus and Grafana

After trying CloudWatch, Datadog, and New Relic, we landed on Prometheus and Grafana for the same reason most teams do. Prometheus is open source, pull-based, and fits Kubernetes without protest. Grafana puts the dashboards on top, talks to almost anything, and costs nothing. Self-hosted, the bill is server time. The commercial options were running us $500 a month and climbing.

setup with Docker Compose

For development or small deployments, Docker Compose is perfect:

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=30d'
    ports:
      - "9090:9090"
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=changeme
      - GF_USERS_ALLOW_SIGN_UP=false
    ports:
      - "3000:3000"
    restart: unless-stopped
    depends_on:
      - prometheus

  node_exporter:
    image: prom/node-exporter:latest
    container_name: node_exporter
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    ports:
      - "9100:9100"
    restart: unless-stopped

volumes:
  prometheus_data:
  grafana_data:

Run it:

docker-compose up -d

Done. Prometheus is on :9090, Grafana on :3000.

prometheus configuration

Create prometheus.yml:

global:
  scrape_interval: 15s
  evaluation_interval: 15s
  external_labels:
    cluster: 'production'
    region: 'us-east-1'

# Alertmanager configuration (we'll get to this)
alerting:
  alertmanagers:
    - static_configs:
        - targets: ['alertmanager:9093']

# Load rules
rule_files:
  - 'alerts/*.yml'

# Scrape configurations
scrape_configs:
  # Prometheus itself
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  # Node metrics (CPU, memory, disk)
  - job_name: 'node'
    static_configs:
      - targets: ['node_exporter:9100']
        labels:
          instance: 'production-server-1'

  # Example: monitoring a web app
  - job_name: 'webapp'
    static_configs:
      - targets: ['webapp:8080']
    metrics_path: '/metrics'

instrumenting your application

Node.js / Express example

const express = require('express');
const promClient = require('prom-client');

const app = express();
const register = new promClient.Registry();

// Collect default metrics (CPU, memory, event loop)
promClient.collectDefaultMetrics({ register });

// Custom metrics
const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.1, 0.5, 1, 2, 5]
});

const httpRequestTotal = new promClient.Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'route', 'status_code']
});

register.registerMetric(httpRequestDuration);
register.registerMetric(httpRequestTotal);

// Middleware to track requests
app.use((req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    const labels = {
      method: req.method,
      route: req.route?.path || req.path,
      status_code: res.statusCode
    };

    httpRequestDuration.observe(labels, duration);
    httpRequestTotal.inc(labels);
  });

  next();
});

// Metrics endpoint
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

app.listen(8080, () => {
  console.log('Server running on :8080');
  console.log('Metrics available at /metrics');
});

Python / Flask example

from flask import Flask, request
from prometheus_client import Counter, Histogram, generate_latest
import time

app = Flask(__name__)

# Metrics
request_count = Counter(
    'http_requests_total',
    'Total HTTP requests',
    ['method', 'endpoint', 'status']
)

request_duration = Histogram(
    'http_request_duration_seconds',
    'HTTP request duration',
    ['method', 'endpoint']
)

@app.before_request
def before_request():
    request.start_time = time.time()

@app.after_request
def after_request(response):
    duration = time.time() - request.start_time

    request_count.labels(
        method=request.method,
        endpoint=request.endpoint,
        status=response.status_code
    ).inc()

    request_duration.labels(
        method=request.method,
        endpoint=request.endpoint
    ).observe(duration)

    return response

@app.route('/metrics')
def metrics():
    return generate_latest()

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

the queries you’ll actually use

CPU usage

# CPU usage percentage
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

# Per core
rate(node_cpu_seconds_total{mode!="idle"}[5m])

memory usage

# Memory usage percentage
(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100

# Available memory
node_memory_MemAvailable_bytes / 1024 / 1024 / 1024

disk usage

# Disk usage percentage
(1 - (node_filesystem_avail_bytes / node_filesystem_size_bytes)) * 100

# Filter to only important mounts
(1 - (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"})) * 100

HTTP request rate

# Requests per second
rate(http_requests_total[5m])

# By status code
sum by (status_code) (rate(http_requests_total[5m]))

HTTP latency

# 95th percentile latency
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))

# Average latency
rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m])

grafana dashboards

import community dashboards

  1. Go to Grafana → Dashboards → Import
  2. Use these IDs:
    • 1860: Node Exporter Full
    • 3662: Prometheus 2.0 Overview
    • 7362: MySQL Overview

Or create custom dashboards with the queries above.

auto-provisioning the datasource

Create grafana/provisioning/datasources/prometheus.yml:

apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: false

alerting that actually works

Create alerts/rules.yml:

groups:
  - name: system_alerts
    interval: 30s
    rules:
      # High CPU
      - alert: HighCPUUsage
        expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High CPU usage on {{ $labels.instance }}"
          description: "CPU usage is {{ $value }}%"

      # High Memory
      - alert: HighMemoryUsage
        expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High memory usage on {{ $labels.instance }}"
          description: "Memory usage is {{ $value }}%"

      # Disk Space
      - alert: DiskSpaceLow
        expr: (1 - (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"})) * 100 > 85
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Disk space low on {{ $labels.instance }}"
          description: "Disk usage is {{ $value }}%"

      # Service Down
      - alert: ServiceDown
        expr: up == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Service {{ $labels.job }} is down"
          description: "{{ $labels.instance }} has been down for 1 minute"

      # High Error Rate
      - alert: HighErrorRate
        expr: (sum(rate(http_requests_total{status_code=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))) * 100 > 5
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "High error rate detected"
          description: "Error rate is {{ $value }}%"

production considerations

data retention

Prometheus stores data locally. Retention is set via command-line flags (which is why the Compose example passes them in command:), not in prometheus.yml:

--storage.tsdb.retention.time=30d
--storage.tsdb.retention.size=50GB

For longer retention, use:

  • Thanos: Distributed Prometheus
  • Grafana Mimir: Multi-tenant, horizontally scalable Prometheus; the actively-maintained successor to Cortex
  • VictoriaMetrics: Drop-in replacement, more efficient

high availability

Run multiple Prometheus instances with identical configs. Use a load balancer or Thanos for deduplication.

security

# Add basic auth
global:
  external_labels:
    cluster: 'production'

# In Grafana datasource
basicAuth: true
basicAuthUser: prometheus
basicAuthPassword: supersecret

Better: Put behind VPN or use mutual TLS.

common gotchas

Five things bite people in production. Scrape interval below 10s is almost never what you want and quietly burns disk. Too many labels balloon cardinality and your queries get slow before you notice. Pick the right metric type: Counter for monotonic, Gauge for point-in-time, Histogram for distributions, and not the other way around. Monitoring without alerting is a screensaver. And the inverse: too many alerts and the on-call learns to ignore them, which is worse than no alerts at all.

the golden signals

If you only monitor four things, monitor Google’s golden signals: latency, traffic, errors, saturation. Latency is how long requests take. Traffic is request rate. Errors is the rate of failed requests. Saturation is how full the system is on CPU, memory, and disk. Everything else is a refinement on these four.

what to add next

Add exporters for the data stores you actually use: MySQL, Redis, Postgres, whichever queue you’re on. Wire Alertmanager to Slack and PagerDuty so the alerts land somewhere a human reads. Write a one-line runbook link in every alert annotation so the page tells the on-call what to do. Back up the Prometheus data directory; the whole point of long retention is gone if a disk failure wipes it. A dashboard you never look at is graphs heating the data center.

Bash Parameter Expansion: String Manipulation Without External Commands

Originally published at harshit.cloud on 2024-12-17.

TIL: Bash Parameter Expansion: String Manipulation Without External Commands

Learned that Bash has built-in string manipulation that’s way faster than calling sed, awk, or cut.

the old way (slow)

filename="document.pdf"
name=$(echo "$filename" | sed 's/.[^.]*$//')  # document
ext=$(echo "$filename" | sed 's/^.*.//')      # pdf

Each echo | sed spawns a new process. Slow in loops.

the new way (fast)

filename="document.pdf"
name="${filename%.*}"    # document
ext="${filename##*.}"    # pdf

No external processes. Pure Bash.

common patterns

remove file extension

file="archive.tar.gz"
echo "${file%.*}"       # archive.tar
echo "${file%%.*}"      # archive (remove all extensions)

% = remove from end (shortest match)
%% = remove from end (longest match)

get file extension

file="archive.tar.gz"
echo "${file#*.}"       # tar.gz
echo "${file##*.}"      # gz (last extension only)

# = remove from start (shortest match)
## = remove from start (longest match)

string replacement

path="/home/user/documents/file.txt"

# Replace first occurrence
echo "${path/user/admin}"        # /home/admin/documents/file.txt

# Replace all occurrences
echo "${path//o/0}"              # /h0me/user/d0cuments/file.txt

# Replace at start
echo "${path/#/home//root}"    # /root/user/documents/file.txt

# Replace at end
echo "${path/%.txt/.md}"         # /home/user/documents/file.md

default values

# Use default if variable is unset or empty
echo "${VAR:-default}"

# Assign default if unset
echo "${VAR:=default}"

# Error if unset
echo "${VAR:?Variable is required}"

# Use alternate value if set
echo "${VAR:+value_if_set}"

substring extraction

text="Hello World"
echo "${text:0:5}"       # Hello (from pos 0, length 5)
echo "${text:6}"         # World (from pos 6 to end)
echo "${text: -5}"       # World (last 5 chars, note the space!)
echo "${text::-6}"       # Hello (remove last 6 chars)

string length

text="Hello World"
echo "${#text}"          # 11

case conversion

text="Hello World"
echo "${text^^}"         # HELLO WORLD (all uppercase)
echo "${text,,}"         # hello world (all lowercase)
echo "${text^}"          # Hello World (first char uppercase)
echo "${text,}"          # hello World (first char lowercase)

real-world example

Before (slow with multiple processes):

#!/bin/bash
for file in *.log; do
    name=$(basename "$file" .log)
    date=$(echo "$name" | cut -d'-' -f1)
    gzip "$file"
    mv "${file}.gz" "archive-${date}.gz"
done

After (fast, pure Bash):

#!/bin/bash
for file in *.log; do
    name="${file%.log}"      # Remove .log
    date="${name%%-*}"       # Get everything before first -
    gzip "$file"
    mv "${file}.gz" "archive-${date}.gz"
done

cheat sheet

Pattern Effect Example
${var%pattern} Remove shortest match from end ${file%.txt}
${var%%pattern} Remove longest match from end ${file%%.*}
${var#pattern} Remove shortest match from start ${file#*/}
${var##pattern} Remove longest match from start ${file##*/}
${var/old/new} Replace first occurrence ${text/foo/bar}
${var//old/new} Replace all occurrences ${text//foo/bar}
${var:offset:length} Substring ${text:0:5}
${#var} Length ${#text}
${var^^} Uppercase ${text^^}
${var,,} Lowercase ${text,,}

why this matters

In a script processing 10,000 files:

  • With external commands: ~5 minutes
  • With parameter expansion: ~10 seconds

That’s a 30x speedup.

one gotcha

Be careful with spaces in substring extraction:

text="Hello"
echo "${text: -3}"    # llo (CORRECT - note the space)
echo "${text:-3}"     # Hello (WRONG - default value syntax!)

This simple trick has made my shell scripts so much faster. No more unnecessary sed/awk/cut calls.

Instagram Graph API ile Takipçi Sayısı ve Hesap Metriklerini Çekmek (Node.js Rehberi)

Sosyal medya analitiği yapan bir araç, içerik zamanlama uygulaması ya da kişisel bir dashboard yazıyorsanız Instagram’ın resmi API’sini kullanmanız gerekir: Instagram Graph API. Bu yazıda sıfırdan kurulum, kimlik doğrulama ve takipçi sayısı / post insights çekme adımlarını Node.js ile anlatacağım.

Önemli not: “Instagram Basic Display API” Aralık 2024’te kapatıldı. Aktif olan tek resmi yol Instagram Graph API‘dir ve Business veya Creator hesabı ile bir Facebook Page bağlantısı gerektirir.

Neden Resmi API?

Tersine mühendislikle çıkarılmış “private API” veya tarayıcı kazıma (scraping) yöntemleri kısa vadede çalışsa da uzun vadede üç sorun çıkarır:

  • Instagram’ın algoritması bot davranışını giderek daha agresif tespit ediyor ve hesabınızı askıya alıyor.
  • Yapı her güncellemede değiştiği için kodunuz sürekli kırılır.
  • Meta Kullanım Koşulları’nı ihlal eder; ticari bir ürün geliştiriyorsanız hukuki risk taşır.

Resmi Graph API yavaş kurulur ama bir kez kurulduğunda istikrarlı, oran sınırları belli ve dokümante edilmiştir.

Ön Koşullar

Başlamadan önce şunlara ihtiyacınız var:

  1. Instagram Business veya Creator hesabı (kişisel hesap çalışmaz — Settings → Account → Switch to Professional Account)
  2. Instagram hesabına bağlı bir Facebook Page
  3. Meta for Developers hesabı
  4. Node.js 18+ kurulu bir geliştirme ortamı

1. Adım: Meta Developer Uygulaması Oluşturmak

developers.facebook.com adresine girip “My Apps” → “Create App” deyin. Uygulama tipi olarak Business seçin. Uygulama oluştuktan sonra Dashboard’da sol menüden “Add Product” kısmında Instagram Graph API‘yi ve Facebook Login‘u ekleyin.

App Settings → Basic kısmından App ID ve App Secret değerlerini bir kenara not edin. Bunları kodda environment variable olarak kullanacaksınız.

2. Adım: Erişim Token’ı Almak

Instagram Graph API üç token türü kullanır:

Token Tipi Geçerlilik Kullanım
Short-lived User Token 1 saat İlk OAuth akışında alınır
Long-lived User Token 60 gün Üretimde kullanılan token
Page Access Token Süresiz* Sayfa adına Instagram işlemleri için

* Page token’ı kullanıcı sayfa yetkilerini değiştirmedikçe geçerli kalır.

OAuth Akışı

Test için en hızlısı Graph API Explorer‘ı kullanmaktır: https://developers.facebook.com/tools/explorer/

Üretim için kendi OAuth akışınızı kurarsınız:

// 1. Kullanıcıyı Facebook login'e yönlendir
const loginUrl = `https://www.facebook.com/v21.0/dialog/oauth?` +
  `client_id=${APP_ID}` +
  `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
  `&scope=instagram_basic,instagram_manage_insights,pages_show_list,pages_read_engagement` +
  `&response_type=code`;

// 2. Geri dönüşte code'u short-lived token'a çevir
async function exchangeCodeForToken(code) {
  const url = `https://graph.facebook.com/v21.0/oauth/access_token?` +
    `client_id=${APP_ID}` +
    `&client_secret=${APP_SECRET}` +
    `&redirect_uri=${REDIRECT_URI}` +
    `&code=${code}`;
  const res = await fetch(url);
  return res.json();
}

// 3. Short-lived token'ı 60 günlük long-lived token'a çevir
async function getLongLivedToken(shortToken) {
  const url = `https://graph.facebook.com/v21.0/oauth/access_token?` +
    `grant_type=fb_exchange_token` +
    `&client_id=${APP_ID}` +
    `&client_secret=${APP_SECRET}` +
    `&fb_exchange_token=${shortToken}`;
  const res = await fetch(url);
  return res.json();
}

3. Adım: Instagram Business Account ID’sini Bulmak

Bir kullanıcının Instagram verilerine erişmek için önce Facebook Page ID‘sini, sonra ona bağlı Instagram Business Account ID‘sini bulmanız gerekir:

async function getInstagramAccountId(userAccessToken) {
  const pagesRes = await fetch(
    `https://graph.facebook.com/v21.0/me/accounts?access_token=${userAccessToken}`
  );
  const pages = await pagesRes.json();
  const pageId = pages.data[0].id;
  const pageToken = pages.data[0].access_token;

  const igRes = await fetch(
    `https://graph.facebook.com/v21.0/${pageId}?fields=instagram_business_account&access_token=${pageToken}`
  );
  const igData = await igRes.json();

  return {
    instagramAccountId: igData.instagram_business_account.id,
    pageAccessToken: pageToken
  };
}

4. Adım: Takipçi Sayısı ve Profil Bilgilerini Çekmek

Asıl olay burada başlıyor. Tek bir istekle username, takipçi sayısı, post sayısı gibi temel bilgileri alabilirsiniz:

async function getAccountInfo(igAccountId, pageToken) {
  const fields = [
    'username',
    'name',
    'biography',
    'followers_count',
    'follows_count',
    'media_count',
    'profile_picture_url'
  ].join(',');

  const url = `https://graph.facebook.com/v21.0/${igAccountId}` +
    `?fields=${fields}&access_token=${pageToken}`;

  const res = await fetch(url);
  return res.json();
}

// Örnek çıktı:
// {
//   username: "ornek_hesap",
//   followers_count: 12453,
//   follows_count: 188,
//   media_count: 247
// }

5. Adım: Post Insights (Etkileşim Verisi)

Belirli bir gönderinin beğeni, yorum, erişim ve gösterim sayılarını almak için:

async function getMediaList(igAccountId, pageToken) {
  const url = `https://graph.facebook.com/v21.0/${igAccountId}/media` +
    `?fields=id,caption,media_type,permalink,timestamp,like_count,comments_count` +
    `&access_token=${pageToken}`;
  const res = await fetch(url);
  return res.json();
}

async function getMediaInsights(mediaId, pageToken) {
  const metrics = 'impressions,reach,saved,engagement';
  const url = `https://graph.facebook.com/v21.0/${mediaId}/insights` +
    `?metric=${metrics}&access_token=${pageToken}`;
  const res = await fetch(url);
  return res.json();
}

6. Adım: Hesap Bazında Insights

Hesabın genelinde günlük/haftalık erişim, profil ziyareti gibi metrikler için:

async function getAccountInsights(igAccountId, pageToken) {
  const metrics = 'impressions,reach,profile_views';
  const period = 'day';
  const url = `https://graph.facebook.com/v21.0/${igAccountId}/insights` +
    `?metric=${metrics}&period=${period}&access_token=${pageToken}`;
  const res = await fetch(url);
  return res.json();
}

Hız Sınırları (Rate Limits)

Instagram Graph API uygulama bazında saatte 200 çağrı × hesap sayısı olarak hesaplanır. Yani 100 işletme hesabınız varsa saatte 20.000 çağrı yapabilirsiniz. Hız sınırını aştığınızda error.code === 4 yanıtı dönüyor.

İyi pratikler:

  • Sık çağırdığınız verileri (örn. takipçi sayısı) en az 15 dakika cache’leyin
  • Insight çağrılarını günde bir kez batch olarak çalıştırın
  • Webhook’larla (instagram_manage_insights) push tabanlı akışa geçin

Sık Karşılaşılan Hatalar

(#10) Application does not have permission for this action — App Review’dan geçmeyen permission istiyorsunuz. Test için kendinizi geliştirici/test kullanıcısı olarak ekleyin.

Invalid OAuth access token — Token süresi dolmuş ya da farklı uygulamaya ait. Long-lived token’ı 50. günde proaktif olarak yenileyin.

Instagram User does not exist — Hesap Business/Creator değil ya da Page’e bağlı değil.

Sonuç

Instagram’ın resmi API’si ilk kurulumda epey kalabalık (Facebook App + Page + Business hesabı + OAuth) ama bir kez ayaklandıktan sonra sağlam bir temel oluşturur. Üzerine; planlanmış yayın aracı, analytics dashboard, içerik öneri sistemi, otomatik DM yanıtlayıcı gibi pek çok proje kurabilirsiniz.

Eğer scraping ya da bot otomasyonu düşünüyorsanız: kısa vadeli kazançtan vazgeçip resmi API’ye yatırım yapın. Üretim ortamında ölçeklenebilen tek yol budur.

Kaynaklar ve Faydalı Bağlantılar

Sorularınızı yorumlara yazabilirsiniz. Bir sonraki yazıda Instagram’ın Webhooks sistemi ile push tabanlı bildirimleri ele alacağım.